Compare commits

...
39 Commits
Author SHA1 Message Date
Glavo a4f5e013cb 修复使用 Java 8 构建失败的问题 (#3326) 2024-10-14 01:24:14 +08:00
Glavo d4579852bb 优化 AboutPage (#3325)
* 优化关于页面

* update
2024-10-14 01:08:06 +08:00
Zkitefly edd95ad626 模组管理搜索操作更改为文本更新后触发 (#3221)
* 更改为文本更新后触发

* 添加防抖
2024-10-13 21:57:32 +08:00
ZkiteflyandGlavo dec307b531 安装新版本的版本名输入框显示已选择加载器名称 (#3234)
* Update InstallersPage.java

* 优化代码

* Update InstallersPage.java

* Update InstallersPage.java

* fix: checkstyle

* 给 LibraryAnalyzer.LibraryType.fromPatchId 稍微优化一下,把她里面的实现改为一个预先生成的 map,

相信你知道怎么把这个函数改成一个 lookupMap.get(patchId) 的形式

* Update InstallersPage.java

* Update InstallersPage.java

* Fix

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-13 21:46:48 +08:00
Glavo 00b7085370 更新 Linux LoongArch64 支持 (#3264) 2024-10-13 20:54:15 +08:00
Glavo 17c1998119 修复对话框标题和按钮未使用自定义字体的问题 (#3324) 2024-10-13 20:39:55 +08:00
Glavo 1c05cd981f 使用 ./gradlew run 启动 HMCL 时解析 HMCL_JAVA_OPTS 环境变量 (#3323) 2024-10-13 20:20:05 +08:00
Glavo e9c3795ffc 预生成 openjfx-dependencies.json (#3322) 2024-10-13 19:52:00 +08:00
Zkiteflyand3gf8jv4dv c3917082ee 添加打开启动器日志文件夹按钮 (#3237)
* 添加打开启动器日志文件夹

* Update HMCL/src/main/resources/assets/lang/I18N.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-13 19:37:35 +08:00
3gf8jv4dv 6265580d75 Adjust the order of sidebars in instance settings (#3278) 2024-10-13 19:35:24 +08:00
YLDK 65fc3d0c4f Support #3087: 隐藏整合包文件位置显示 (#3088)
* Support #3087: 隐藏整合包文件位置显示

* Support #3087: 移除整合包文件位置显示代码

* Support #3087: 移除整合包文件位置显示代码
2024-10-13 19:29:02 +08:00
Glavo 1c864ba072 使用 Jsoup 解析并渲染 HTML 页面 (#3321) 2024-10-13 19:19:52 +08:00
Glavo ef9159666c 使用 TypeToken 替代 Type (#3320) 2024-10-11 20:50:58 +08:00
Glavo 541f2e2619 清理不再使用的 i18n 条目 (#3318) 2024-10-09 02:00:53 +08:00
Glavo c7f248a092 优化更新窗口 (#3317)
* 优化更新窗口

* fix NPE
2024-10-09 01:40:26 +08:00
Zkitefly 7937f8ddf5 发现更新弹窗添加取消按钮 (#3282) 2024-10-09 01:03:56 +08:00
Glavo 74926704bd Close #3209: 搜索 IntelliJ IDEA 安装的 Java (#3316) 2024-10-09 00:35:07 +08:00
Glavo 9196bda537 优化 FXUtils.testLinuxCommand (#3315) 2024-10-09 00:19:06 +08:00
Glavo 4c1e607b8e 在 Linux/FreeBSD 平台读取 os-release (#3314)
* 简化 OperatingSystem.getPhysicalMemoryStatus

* Read os-release

* update

* update
2024-10-07 00:57:35 +08:00
Zkitefly 26224ae366 删除账户和删除认证服务器时弹出提示确认删除 (#3280) 2024-10-06 23:38:48 +08:00
Glavo d3e9511aca 删除 FractureiserDetector (#3313) 2024-10-06 23:31:19 +08:00
ShulkerSakura b624cebc08 修改下载页面按钮名称 (#3262) 2024-10-06 23:21:12 +08:00
Glavo 7e4d437a1d 重构 Java 管理 (#2988)
* update

* update

* Update task name

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* Update logo
2024-10-05 23:50:14 +08:00
Zkitefly 37d6857b82 根据版本类型调整下载项图标 (#3306) 2024-10-03 00:31:06 +08:00
Zkitefly f8d7c92d51 Close #3208: 隐藏版本更新界面的Fabric/Quilt API选项 (#3271) 2024-09-24 03:32:02 +08:00
YSXX1013andGlavo ea9999a1f9 支持使用 CMake 与 MinGW 构建 HMCLauncher (#3011)
* 尝试性的为HMCLauncher添加cmake及gcc支持

* 完善cmake及gcc支持

* 尝试支持Github Actions

* 尝试支持Github Actions-2

* 尝试支持Github Actions-3

* 尝试支持Github Actions-4

* 尝试支持Github Actions-5

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-24 02:51:44 +08:00
3gf8jv4dv d969fab284 Fix checkstyle (#3292) 2024-09-19 17:42:58 +08:00
vochant 97fe52c2a7 feat: Supported JRE from SDKMAN! on Linux (#3062) 2024-09-19 03:09:05 +08:00
Glavo 8a816f7f35 优化 LogWindow (#3274)
* update

* update

* update

* update

* update
2024-09-02 20:54:33 +08:00
6409841cca Fix #3138: HMCL 错误地将 Authlib 日志识别为 ERROR (#3142)
* Close#3138

* 恢复gradle-wrapper.properties

* 恢复gradle-wrapper.properties

* 添加空格

* 修复控制台输出的日志

* Merge if statement

* update

* update

---------

Co-authored-by: CiLuQwQ <a@>
Co-authored-by: burningtnt <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-02 01:51:53 +08:00
ZkiteflyandGlavo 6348166dac 支持在 Linux/BSD 平台使用 showFileInExplorer (#3233)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update FXUtils.java

* Update ModListPageSkin.java

* 使用 dbus-send

* Update FXUtils.java

* Update FXUtils.java

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-01 00:27:06 +08:00
ZkiteflyandGlavo 48417b9b7b 显示 Forge 发版时间 (#3256)
* 显示时间

* Fix

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-31 00:44:43 +08:00
Burning_TNT 738000ff39 移除无用类与方法 (#3132)
* Remove useless classes.

* Remove useless methods
2024-08-25 03:03:48 +08:00
ZkiteflyandGlavo 9b5445b686 修复模组详情页图标问题 (#3227)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* update

* 调整判断逻辑

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-25 02:55:55 +08:00
Haowei Wen abf58b2c20 Fix missing i18n key curse.category.7418 (#3260) 2024-08-24 23:44:13 +08:00
Zkitefly fd97f6c321 支持 Forge 官方源 (#3259)
* Revert "Revert "支持 Forge 官方源 (#3251)" (#3258)"

This reverts commit abd37a093a.

* Update ForgeVersionList.java
2024-08-24 23:43:50 +08:00
Glavo abd37a093a Revert "支持 Forge 官方源 (#3251)" (#3258)
This reverts commit 0f0cf55509.
2024-08-24 14:50:41 +08:00
Zkitefly 0f0cf55509 支持 Forge 官方源 (#3251)
* 支持 Forge 官方源

* 更新 ForgeVersionList.java

* 更新 ForgeVersionList.java
2024-08-24 03:32:45 +08:00
ZkiteflyandGlavo 95a1496389 Fix #3223: 正确解析包含 hash 的版本号 (#3224)
* Fix #3223

* 使用正则表达式删除内容

* 添加日志

添加日志

* Update GameVersion.java

* Update GameVersion.java

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-20 01:08:49 +08:00
148 changed files with 7299 additions and 2410 deletions

No files matched your search

+57 -5
View File
@@ -189,11 +189,6 @@ tasks.processResources {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
into("assets") {
from(project.layout.buildDirectory.file("openjfx-dependencies.json"))
}
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
}
val makeExecutables = tasks.create("makeExecutables") {
@@ -208,6 +203,56 @@ tasks.build {
dependsOn(makeExecutables)
}
fun parseToolOptions(options: String?): List<String> {
if (options == null)
return listOf()
val builder = StringBuilder()
val result = mutableListOf<String>()
var offset = 0
loop@ while (offset < options.length) {
val ch = options[offset]
if (Character.isWhitespace(ch)) {
if (builder.isNotEmpty()) {
result += builder.toString()
builder.clear()
}
while (offset < options.length && Character.isWhitespace(options[offset])) {
offset++
}
continue@loop
}
if (ch == '\'' || ch == '"') {
offset++
while (offset < options.length) {
val ch2 = options[offset++]
if (ch2 != ch) {
builder.append(ch2)
} else {
continue@loop
}
}
throw GradleException("Unmatched quote in $options")
}
builder.append(ch)
offset++
}
if (builder.isNotEmpty()) {
result += builder.toString()
}
return result
}
tasks.create<JavaExec>("run") {
dependsOn(tasks.jar)
@@ -215,4 +260,11 @@ tasks.create<JavaExec>("run") {
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
jvmArgs(vmOptions)
doFirst {
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
}
}
@@ -222,7 +222,9 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
@@ -20,10 +20,9 @@ package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.FractureiserDetector;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
@@ -61,7 +60,7 @@ public final class Main {
checkDirectoryPath();
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
@@ -70,7 +69,6 @@ public final class Main {
checkJavaFX();
verifyJavaFX();
detectFractureiser();
Launcher.main(args);
}
@@ -94,13 +92,6 @@ public final class Main {
}
}
private static void detectFractureiser() {
if (FractureiserDetector.detect()) {
LOG.error("Detected that this computer is infected by fractureiser");
showErrorAndExit(i18n("fatal.fractureiser"));
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
@@ -1,86 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Locale;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Countly {
private String deviceId;
private String endpoint;
private String serverURL;
public void sendMetric(String metrics) throws IOException {
HttpRequest.GET(serverURL + endpoint,
pair("begin_session", "1"),
pair("session_id", "1"),
pair("metrics", metrics),
pair("device_id", deviceId),
pair("timestamp", Long.toString(System.currentTimeMillis())),
pair("tz", Integer.toString(getTimezoneOffset())),
pair("hour", Integer.toString(currentHour())),
pair("dow", Integer.toString(currentDayOfWeek())),
pair("app_key", APP_KEY),
pair("sdk_name", "java-native"),
pair("sdk_version", "20.11.1"))
.getString();
}
private static int getTimezoneOffset() {
return ZonedDateTime.now().getOffset().getTotalSeconds() / 60;
}
private static String getLocale() {
final Locale locale = Locale.getDefault();
return locale.getLanguage() + "_" + locale.getCountry();
}
private static int currentHour() {
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
private int currentDayOfWeek() {
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (day) {
case Calendar.SUNDAY:
return 0;
case Calendar.MONDAY:
return 1;
case Calendar.TUESDAY:
return 2;
case Calendar.WEDNESDAY:
return 3;
case Calendar.THURSDAY:
return 4;
case Calendar.FRIDAY:
return 5;
case Calendar.SATURDAY:
return 6;
}
return 0;
}
private static final String APP_KEY = "";
}
@@ -5,14 +5,8 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class CrashReport {
@@ -20,24 +14,16 @@ public class CrashReport {
private final Throwable throwable;
private final String stackTrace;
private boolean nonFatal;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
nonFatal = false;
}
public Throwable getThrowable() {
return this.throwable;
}
public CrashReport setNonFatal() {
nonFatal = true;
return this;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
@@ -48,23 +34,6 @@ public class CrashReport {
return true;
}
public Map<String, Object> getMetrics(long runningTime) {
return mapOf(
pair("_run", runningTime),
pair("_app_version", Metadata.VERSION),
pair("_os", OperatingSystem.SYSTEM_NAME),
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
pair("_disk_current", getDiskAvailable()),
pair("_disk_total", getDiskTotal()),
pair("_ram_current", getMemoryAvailable()),
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
pair("_error", stackTrace),
pair("_logs", LOG.getLogs()),
pair("_name", throwable.getLocalizedMessage()),
pair("_nonfatal", nonFatal)
);
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
@@ -82,29 +51,4 @@ public class CrashReport {
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
private static final Long BYTES_IN_MB = 1024L * 1024;
private static long getMemoryAvailable() {
Long total = Runtime.getRuntime().totalMemory();
Long availMem = Runtime.getRuntime().freeMemory();
return (total - availMem) / BYTES_IN_MB;
}
private static long getDiskAvailable() {
long total = 0, free = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
free += f.getUsableSpace();
}
return (total - free) / BYTES_IN_MB;
}
private static long getDiskTotal() {
long total = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
}
return total / BYTES_IN_MB;
}
}
@@ -37,7 +37,7 @@ import org.jackhuang.hmcl.util.Lang;
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.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -383,7 +383,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -396,7 +396,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
@@ -67,8 +66,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), 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.");
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
@@ -28,15 +27,13 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
@@ -55,19 +52,17 @@ import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.platform.Platform.*;
public final class LauncherHelper {
@@ -132,12 +127,12 @@ public final class LauncherHelper {
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), javaVersion, setting));
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), java, setting));
if (setting.isNotCheckGame())
return null;
return Task.allOf(
@@ -155,7 +150,7 @@ public final class LauncherHelper {
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(javaVersion, renderer);
Library lib = NativePatcher.getMesaLoader(java, renderer);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -179,9 +174,7 @@ public final class LauncherHelper {
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null);
})
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
@@ -214,7 +207,7 @@ public final class LauncherHelper {
it.fireEvent(new DialogCloseEvent());
}));
} else {
Platform.runLater(() -> {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
@@ -234,7 +227,7 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
Platform.runLater(() -> {
runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
@@ -331,178 +324,181 @@ public final class LauncherHelper {
executor.start();
}
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
private static Task<JavaRuntime> checkGameState(Profile profile, VersionSetting setting, Version version) {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
try {
return setting.getJava(gameVersion, version);
} catch (InterruptedException e) {
throw new CancellationException();
}
});
Task<JavaRuntime> task;
if (setting.isNotCheckJVM()) {
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
.withStage("launch.state.java");
}
task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault()));
} else if (setting.getJavaVersionType() == JavaVersionType.AUTO || setting.getJavaVersionType() == JavaVersionType.VERSION) {
task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> {
if (java != null) {
return Task.completed(java);
}
return Task.composeAsync(() -> {
return setting.getJavaVersion(gameVersion, version);
}).thenComposeAsync(Schedulers.javafx(), javaVersion -> {
// Reset invalid java version
if (javaVersion == null) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
// Reset invalid java version
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("No accepted java"));
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM);
if (setting.isJavaAutoSelected()) {
GameJavaVersion targetJavaVersion = null;
GameJavaVersion targetJavaVersion = null;
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
try {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
if (range.getMandatory().contains(VersionNumber.asVersion("21.0.3"))) {
targetJavaVersion = GameJavaVersion.JAVA_21;
} else if (range.getMandatory().contains(VersionNumber.asVersion("17.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_17;
} else if (range.getMandatory().contains(VersionNumber.asVersion("16.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else {
String java8Version;
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
Controllers.dialog(
i18n("launch.failed.java_version_too_low"),
i18n("message.error"),
MessageType.ERROR,
breakAction
);
return result;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
java8Version = "1.8.0_51";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
java8Version = "1.8.0_202";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
java8Version = "1.8.0_74";
} else {
java8Version = null;
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
} catch (NumberFormatException ignored) {
}
} else
targetJavaVersion = version.getJavaVersion();
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
.whenCompleteAsync((downloadedJava, exception) -> {
if (exception == null) {
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", exception);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
}, Schedulers.javafx());
} else {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
return result;
});
} else {
task = getJavaTask.thenComposeAsync(java -> {
Set<JavaVersionConstraint> violatedMandatoryConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
Set<JavaVersionConstraint> violatedSuggestedConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
if (java != null) {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violatedMandatoryConstraints.add(constraint);
} else {
violatedSuggestedConstraints.add(constraint);
}
}
if (java8Version != null && range.getMandatory().contains(VersionNumber.asVersion(java8Version)))
targetJavaVersion = GameJavaVersion.JAVA_8;
else
targetJavaVersion = null;
}
}
}
if (targetJavaVersion == null) {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version);
if (suggestedJava != null) {
FXUtils.runInFX(() -> {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJava);
}, breakAction);
});
return result;
} else if (java == null) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.invalid_java"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
} else {
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
.thenAcceptAsync(downloadedJavaVersion -> {
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
else
gameJavaVersion = null;
if (gameJavaVersion != null) {
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> {
if (throwable == null) {
setting.setJavaAutoSelected();
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", throwable);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
});
return null;
});
}
} else {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
breakAction.run();
}
}, Schedulers.javafx()));
return result;
}
setting.setJava(null);
setting.setDefaultJavaPath(null);
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
}
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
violatedMandatoryConstraints.remove(JavaVersionConstraint.VANILLA_LINUX_JAVA_8);
}
}
return Task.fromCompletableFuture(future);
} else {
return Task.completed(javaVersion);
}
}).thenComposeAsync(javaVersion -> {
return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version)));
}).thenComposeAsync(Schedulers.javafx(), javaVersions -> {
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.LAUNCH_WRAPPER)) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
JavaVersionConstraint violatedMandatoryConstraint = null;
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
violatedMandatoryConstraint = constraint;
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
if (violatedSuggestedConstraints == null)
violatedSuggestedConstraints = new ArrayList<>(1);
violatedSuggestedConstraints.add(constraint);
if (!violatedMandatoryConstraints.isEmpty()) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.unknown") + "\n" + violatedMandatoryConstraints,
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
}
}
}
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
List<String> suggestions = new ArrayList<>();
if (violatedMandatoryConstraint != null) {
if (suggestedJavaVersion != null) {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJavaVersion);
}, breakAction);
return Task.fromCompletableFuture(future);
} else {
switch (violatedMandatoryConstraint) {
case GAME_JSON:
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
.thenAcceptAsync(downloadedJavaVersion -> {
setting.setJavaVersion(downloadedJavaVersion);
future.complete(downloadedJavaVersion);
}, Schedulers.javafx())
.whenCompleteAsync((result, throwable) -> {
LOG.warning("Failed to download java", throwable);
breakAction.run();
}, Schedulers.javafx());
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_16:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_17:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_21:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 21), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
case VANILLA_LINUX_JAVA_8:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
} else {
break;
}
case LAUNCH_WRAPPER:
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && java.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
}
List<String> suggestions = new ArrayList<>(0);
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
// 32-bit JVM cannot make use of too much memory.
if (javaVersion.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (violatedSuggestedConstraints != null) {
for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) {
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
@@ -510,7 +506,7 @@ public final class LauncherHelper {
break;
case MODDED_JAVA_8:
// Minecraft>=1.7.10+Forge accepts Java 8
if (javaVersion.getParsedVersion() < 8)
if (java.getParsedVersion() < 8)
suggestions.add(i18n("launch.advice.newer_java"));
else
suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion));
@@ -534,117 +530,91 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
&& isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
default:
suggestions.add(violatedSuggestedConstraint.name());
}
}
}
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
VersionNumber forgeVersion = version.getLibraries().stream()
.filter(it -> it.is("net.minecraftforge", "forge"))
.findFirst()
.map(library -> VersionNumber.asVersion(library.getVersion()))
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(javaVersion);
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
} else {
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
.map(VersionNumber::asVersion)
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(java);
}
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
}
FXUtils.runInFX(() -> Controllers.confirm(
message,
i18n("message.warning"),
MessageType.WARNING,
() -> future.complete(java),
breakAction));
}
Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction);
}
return result;
});
}
return Task.fromCompletableFuture(future);
}).withStage("launch.state.java");
return task.withStage("launch.state.java");
}
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
link.setOnAction(e -> {
if (javaVersion.getMajorVersion() == JavaVersion.JAVA_8) {
FXUtils.openLink(ORACLEJDK_DOWNLOAD_LINK);
} else {
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}
future.completeExceptionally(new CancellationException());
});
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version",
gameVersion,
javaVersion.getMajorVersion()),
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.addAction(link)
.yesOrNo(() -> {
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
.thenAcceptAsync(future::complete)
.exceptionally(throwable -> {
Throwable resolvedException = resolveException(throwable);
LOG.warning("Failed to download java", throwable);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider();
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
future.complete(result);
} else {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
future.completeExceptionally(new CancellationException());
}
future.completeExceptionally(new CancellationException());
return null;
});
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
}, () -> future.completeExceptionally(new CancellationException())).build());
return future;
}
/**
* Directly start java downloading.
*
* @param javaVersion target Java version
* @param downloadProvider download provider
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
*/
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Controllers.taskDialog(JavaRepository.downloadJava(javaVersion, downloadProvider)
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
if (exception != null) {
future.completeExceptionally(exception);
} else {
future.complete(downloadedJava);
}
}), i18n("download.java"), TaskCancellationAction.NORMAL);
return future;
}
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
@@ -685,7 +655,7 @@ public final class LauncherHelper {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
@@ -696,9 +666,9 @@ public final class LauncherHelper {
case CLOSE:
throw new Error("Never get to here");
case HIDE:
Platform.runLater(() -> {
runLater(() -> {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
setImplicitExit(true);
// If we use Launcher.stop(), log window will be halt immediately.
Launcher.stopWithoutPlatform();
});
@@ -717,14 +687,14 @@ public final class LauncherHelper {
private final Version version;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private volatile boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final ArrayDeque<String> logs;
private final ArrayDeque</*Log4jLevel*/Object> levels;
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CircularArrayList<Log> logs;
private final CountDownLatch launchingLatch;
private final String forbiddenAccessToken;
private Thread submitLogThread;
private LinkedBlockingQueue<Log> logBuffer;
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
@@ -733,10 +703,7 @@ public final class LauncherHelper {
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
final int numLogs = config().getLogLines() + 1;
this.logs = new ArrayDeque<>(numLogs);
this.levels = new ArrayDeque<>(numLogs);
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
}
@Override
@@ -752,18 +719,66 @@ public final class LauncherHelper {
LOG.info("Process ClassPath: " + classpath);
}
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow(process);
logWindow.showNormal();
if (showLogs) {
CountDownLatch logWindowLatch = new CountDownLatch(1);
runLater(() -> {
logWindow = new LogWindow(process, logs);
logWindow.show();
logWindowLatch.countDown();
});
logBuffer = new LinkedBlockingQueue<>();
submitLogThread = Lang.thread(new Runnable() {
private final ArrayList<Log> currentLogs = new ArrayList<>();
private final Semaphore semaphore = new Semaphore(0);
private void submitLogs() {
if (currentLogs.size() == 1) {
Log log = currentLogs.get(0);
runLater(() -> logWindow.logLine(log));
} else {
runLater(() -> {
logWindow.logLines(currentLogs);
semaphore.release();
});
semaphore.acquireUninterruptibly();
}
currentLogs.clear();
}
@Override
public void run() {
while (true) {
try {
currentLogs.add(logBuffer.take());
//noinspection BusyWait
Thread.sleep(200); // Wait for more logs
} catch (InterruptedException e) {
break;
}
logBuffer.drainTo(currentLogs);
submitLogs();
}
do {
submitLogs();
} while (logBuffer.drainTo(currentLogs) > 0);
}
}, "Game Log Submitter", true);
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void finishLaunch() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -776,11 +791,11 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
Platform.runLater(launchingLatch::countDown);
runLater(launchingLatch::countDown);
break;
case HIDE:
launchingLatch.countDown();
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -796,44 +811,37 @@ public final class LauncherHelper {
@Override
public void onLog(String log, boolean isErrorStream) {
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
if (isErrorStream)
System.err.println(filteredLog);
System.err.println(log);
else
System.out.println(filteredLog);
System.out.println(log);
Log4jLevel level;
if (isErrorStream)
level = Log4jLevel.ERROR;
else
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
synchronized (this) {
logs.add(filteredLog);
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
if (logs.size() > config().getLogLines()) {
logs.removeFirst();
levels.removeFirst();
}
}
log = StringUtils.parseEscapeSequence(log);
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
if (showLogs) {
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
if (level == null)
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
}
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl) {
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
lwjgl = true;
finishLaunch();
synchronized (this) {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
}
}
}
}
@@ -841,7 +849,13 @@ public final class LauncherHelper {
@Override
public void onExit(int exitCode, ExitType exitType) {
if (showLogs) {
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
submitLogThread.interrupt();
try {
submitLogThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
launchingLatch.countDown();
@@ -850,14 +864,16 @@ public final class LauncherHelper {
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (!lwjgl) {
synchronized (this) {
if (!lwjgl)
finishLaunch();
}
}
if (exitType != ExitType.NORMAL) {
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
Lang.forEachZipped(logs, levels,
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}
checkExit();
@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.Log4jLevel;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Log {
public static final int DEFAULT_LOG_LINES = 2000;
public static int getLogLines() {
Integer lines = config().getLogLines();
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
}
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
}
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
public String getLog() {
return log;
}
public Log4jLevel getLevel() {
Log4jLevel level = this.level;
if (level == null) {
level = Log4jLevel.guessLevel(log);
if (level == null)
level = Log4jLevel.INFO;
this.level = level;
}
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
@@ -141,8 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -0,0 +1,221 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaRemoteFiles;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
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.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HMCLJavaRepository implements JavaRepository {
public static final String MOJANG_JAVA_PREFIX = "mojang-";
private final Path root;
public HMCLJavaRepository(Path root) {
this.root = root;
}
public Path getPlatformRoot(Platform platform) {
return root.resolve(platform.toString());
}
@Override
public Path getJavaDir(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name);
}
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Path getManifestFile(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name + ".json");
}
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public boolean isInstalled(Platform platform, String name) {
return Files.exists(getManifestFile(platform, name));
}
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public @Nullable Path getJavaExecutable(Platform platform, String name) {
Path javaDir = getJavaDir(platform, name);
try {
return JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException ignored) {
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
try {
return JavaManager.getMacExecutable(javaDir).toRealPath();
} catch (IOException ignored1) {
}
}
}
return null;
}
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path root = getPlatformRoot(platform);
if (!Files.isDirectory(root))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
for (Path file : stream) {
try {
String name = file.getFileName().toString();
if (name.endsWith(".json") && Files.isRegularFile(file)) {
Path javaDir = file.resolveSibling(name.substring(0, name.length() - ".json".length()));
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), true));
}
}
} catch (Throwable e) {
LOG.warning("Failed to parse " + file, e);
}
}
} catch (IOException ignored) {
}
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfo.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.getComponent());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
files.put(path, new JavaLocalFiles.LocalFile(downloadInfo.getSha1(), downloadInfo.getSize()));
}
} else if (file instanceof MojangJavaRemoteFiles.RemoteDirectory) {
files.put(path, new JavaLocalFiles.LocalDirectory());
} else if (file instanceof MojangJavaRemoteFiles.RemoteLink) {
files.put(path, new JavaLocalFiles.LocalLink(((MojangJavaRemoteFiles.RemoteLink) file).getTarget()));
}
});
JavaManifest manifest = new JavaManifest(info, update, files);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
Path javaDir = getJavaDir(platform, name);
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
if (!result.getInfo().getPlatform().equals(platform))
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));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@Override
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
return Task.runAsync(() -> {
Files.deleteIfExists(getManifestFile(platform, name));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
});
}
@Override
public Task<Void> getUninstallJavaTask(JavaRuntime java) {
return Task.runAsync(() -> {
Path root = getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
String name = relativized.getName(0).toString();
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
}
});
}
}
@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Glavo
*/
public final class JavaInstallTask extends Task<JavaManifest> {
private final Path targetDir;
private final Map<String, Object> update;
private final Path archiveFile;
private final Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
private final ArrayList<String> nameStack = new ArrayList<>();
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
private final MessageDigest messageDigest = DigestUtils.getDigest("SHA-1");
public JavaInstallTask(Path targetDir, Map<String, Object> update, Path archiveFile) {
this.targetDir = targetDir;
this.update = update;
this.archiveFile = archiveFile;
}
@Override
public void execute() throws Exception {
JavaInfo info;
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(archiveFile)) {
info = JavaInfo.fromArchive(tree);
copyDirContent(tree, targetDir);
}
setResult(new JavaManifest(info, update, files));
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, Path targetDir) throws IOException {
copyDirContent(tree, tree.getRoot().getSubDirs().values().iterator().next(), targetDir);
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, ArchiveFileTree.Dir<E> dir, Path targetDir) throws IOException {
Files.createDirectories(targetDir);
for (Map.Entry<String, E> pair : dir.getFiles().entrySet()) {
Path path = targetDir.resolve(pair.getKey());
E entry = pair.getValue();
nameStack.add(pair.getKey());
if (tree.isLink(entry)) {
String linkTarget = tree.getLink(entry);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalLink(linkTarget));
Files.createSymbolicLink(path, Paths.get(linkTarget));
} else {
long size = 0L;
try (InputStream input = tree.getInputStream(entry);
OutputStream output = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
messageDigest.reset();
int c;
while ((c = input.read(buffer)) > 0) {
size += c;
output.write(buffer, 0, c);
messageDigest.update(buffer, 0, c);
}
}
if (tree.isExecutable(entry))
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
}
nameStack.remove(nameStack.size() - 1);
}
for (Map.Entry<String, ArchiveFileTree.Dir<E>> pair : dir.getSubDirs().entrySet()) {
nameStack.add(pair.getKey());
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalDirectory());
copyDirContent(tree, pair.getValue(), targetDir.resolve(pair.getKey()));
nameStack.remove(nameStack.size() - 1);
}
}
}
@@ -0,0 +1,128 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import java.lang.reflect.Type;
/**
* @author Glavo
*/
public final class JavaLocalFiles {
@JsonAdapter(Serializer.class)
public abstract static class Local {
private final String type;
Local(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public static final class LocalFile extends Local {
private final String sha1;
private final long size;
public LocalFile(String sha1, long size) {
super("file");
this.sha1 = sha1;
this.size = size;
}
public String getSha1() {
return sha1;
}
public long getSize() {
return size;
}
}
public static final class LocalDirectory extends Local {
public LocalDirectory() {
super("directory");
}
}
public static final class LocalLink extends Local {
private final String target;
public LocalLink(String target) {
super("link");
this.target = target;
}
public String getTarget() {
return target;
}
}
public static class Serializer implements JsonSerializer<Local>, JsonDeserializer<Local> {
@Override
public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("type", src.getType());
if (src instanceof LocalFile) {
obj.addProperty("sha1", ((LocalFile) src).getSha1());
obj.addProperty("size", ((LocalFile) src).getSize());
} else if (src instanceof LocalLink) {
obj.addProperty("target", ((LocalLink) src).getTarget());
}
return obj;
}
@Override
public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
JsonObject obj = json.getAsJsonObject();
if (!obj.has("type"))
throw new JsonParseException(json.toString());
String type = obj.getAsJsonPrimitive("type").getAsString();
try {
switch (type) {
case "file": {
String sha1 = obj.getAsJsonPrimitive("sha1").getAsString();
long size = obj.getAsJsonPrimitive("size").getAsLong();
return new LocalFile(sha1, size);
}
case "directory": {
return new LocalDirectory();
}
case "link": {
String target = obj.getAsJsonPrimitive("target").getAsString();
return new LocalLink(target);
}
default:
throw new AssertionError("unknown type: " + type);
}
} catch (Throwable e) {
throw new JsonParseException(json.toString());
}
}
}
}
@@ -0,0 +1,698 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.JavaVersionConstraint;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManager {
private JavaManager() {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "windows-x86";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "windows-x64";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "windows-arm64";
}
} else if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "linux-i386";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "linux";
}
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "mac-os";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "mac-os-arm64";
}
}
return null;
}
public static Path getExecutable(Path javaHome) {
return javaHome.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable());
}
public static Path getMacExecutable(Path javaHome) {
return javaHome.resolve("jre.bundle/Contents/Home/bin/java");
}
public static boolean isCompatible(Platform platform) {
if (platform.getOperatingSystem() != OperatingSystem.CURRENT_OS)
return false;
Architecture architecture = platform.getArchitecture();
if (architecture == Architecture.SYSTEM_ARCH || architecture == Architecture.CURRENT_ARCH)
return true;
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
}
return false;
}
private static volatile Map<Path, JavaRuntime> allJava;
private static final CountDownLatch LATCH = new CountDownLatch(1);
private static final ObjectProperty<Collection<JavaRuntime>> allJavaProperty = new SimpleObjectProperty<>();
private static Map<Path, JavaRuntime> getAllJavaMap() throws InterruptedException {
Map<Path, JavaRuntime> map = allJava;
if (map == null) {
LATCH.await();
map = allJava;
}
return map;
}
private static void updateAllJavaProperty(Map<Path, JavaRuntime> javaRuntimes) {
JavaRuntime[] array = javaRuntimes.values().toArray(new JavaRuntime[0]);
Arrays.sort(array);
allJavaProperty.set(Arrays.asList(array));
}
public static boolean isInitialized() {
return allJava != null;
}
public static Collection<JavaRuntime> getAllJava() throws InterruptedException {
return getAllJavaMap().values();
}
public static ObjectProperty<Collection<JavaRuntime>> getAllJavaProperty() {
return allJavaProperty;
}
public static JavaRuntime getJava(Path executable) throws IOException, InterruptedException {
executable = executable.toRealPath();
JavaRuntime javaRuntime = getAllJavaMap().get(executable);
if (javaRuntime != null) {
return javaRuntime;
}
JavaInfo info = JavaInfo.fromExecutable(executable);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
updateAllJavaProperty(result);
}
}).start();
}
public static Task<JavaRuntime> getAddJavaTask(Path binary) {
return Task.supplyAsync("Get Java", () -> JavaManager.getJava(binary))
.thenApplyAsync(Schedulers.javafx(), javaRuntime -> {
if (!JavaManager.isCompatible(javaRuntime.getPlatform())) {
throw new UnsupportedPlatformException("Incompatible platform: " + javaRuntime.getPlatform());
}
String pathString = javaRuntime.getBinary().toString();
ConfigHolder.globalConfig().getDisabledJava().remove(pathString);
if (ConfigHolder.globalConfig().getUserJava().add(pathString)) {
addJava(javaRuntime);
}
return javaRuntime;
});
}
public static Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
assert java.isManaged();
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
FXUtils.runInFX(() -> {
try {
removeJava(java);
} catch (InterruptedException e) {
throw new AssertionError("Unreachable code", e);
}
});
String name = relativized.getName(0).toString();
return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name);
} else {
return Task.completed(null);
}
}
// FXThread
public static void addJava(JavaRuntime java) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (!oldMap.containsKey(java.getBinary())) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.put(java.getBinary(), java);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
// FXThread
public static void removeJava(JavaRuntime java) throws InterruptedException {
removeJava(java.getBinary());
}
// FXThread
public static void removeJava(Path realPath) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (oldMap.containsKey(realPath)) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.remove(realPath);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
}
@Nullable
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
return findSuitableJava(getAllJava(), gameVersion, version);
}
@Nullable
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, Version version) {
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
if (forceX86) {
if (!java.getArchitecture().isX86())
continue;
} else {
if (java.getArchitecture() != Architecture.SYSTEM_ARCH)
continue;
}
boolean violationMandatory = false;
boolean violationSuggested = false;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violationMandatory = true;
} else {
violationSuggested = true;
}
}
}
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
}
}
return suggested != null ? suggested : mandatory;
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
}
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
}
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
}
break;
case LINUX:
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
break;
case OSX:
tryAddJavaHome(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines/Contents/Home"));
tryAddJavaHome(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines/Contents/Home"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// Homebrew
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
for (Path dir : dirs) {
searchAllJavaInDirectory(javaRuntimes, dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
}
break;
default:
break;
}
// Search Minecraft bundled runtimes
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
}
if (System.getenv("HMCL_JRES") != null) {
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
} catch (InvalidPathException ignored) {
}
}
}
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
}
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
javaRuntimes.put(currentJava.getBinary(), currentJava);
}
LOG.trace(javaRuntimes.values().stream().sorted()
.map(it -> String.format(" - %s %s (%s, %s): %s",
it.isJDK() ? "JDK" : "JRE",
it.getVersion(),
it.getPlatform().getArchitecture().getDisplayName(),
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
it.getBinary()))
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
return javaRuntimes;
}
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
}
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
try {
info = JavaInfo.fromExecutable(executable, false);
} catch (IOException e2) {
e2.addSuppressed(e);
LOG.warning("Failed to lookup Java executable at " + executable, e2);
}
}
}
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfo.fromExecutable(executable);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
if (info != null && isCompatible(info.getPlatform())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
}
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
tryAddJavaHome(javaRuntimes, dir);
}
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_X86_64), verify);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Paths.get(programFiles);
} catch (InvalidPathException ignored) {
return;
}
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
private static List<String> querySubFolders(String location) {
List<String> res = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith(location) && !line.equals(location)) {
res.add(line);
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query sub folders of " + location, e);
}
return res;
}
private static String queryRegisterValue(String location, String name) {
boolean last = false;
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (StringUtils.isNotBlank(line)) {
if (last && line.trim().startsWith(name)) {
int begins = line.indexOf(name);
if (begins > 0) {
String s2 = line.substring(begins + name.length());
begins = s2.indexOf("REG_SZ");
if (begins > 0) {
return s2.substring(begins + "REG_SZ".length()).trim();
}
}
}
if (location.equals(line.trim())) {
last = true;
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query register value of " + location, e);
}
return null;
}
}
@@ -0,0 +1,112 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
/**
* @author Glavo
*/
@JsonAdapter(JavaManifest.Serializer.class)
public final class JavaManifest {
private final JavaInfo info;
@Nullable
private final Map<String, Object> update;
@Nullable
private final Map<String, JavaLocalFiles.Local> files;
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
this.info = info;
this.update = update;
this.files = files;
}
public JavaInfo getInfo() {
return info;
}
public Map<String, Object> getUpdate() {
return update;
}
public Map<String, JavaLocalFiles.Local> getFiles() {
return files;
}
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
private static final Type LOCAL_FILES_TYPE = mapTypeOf(String.class, JavaLocalFiles.Local.class).getType();
@Override
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject res = new JsonObject();
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.getInfo().getVersion());
res.addProperty("java.vendor", src.getInfo().getVendor());
if (src.getUpdate() != null)
res.add("update", context.serialize(src.getUpdate()));
if (src.getFiles() != null)
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
return res;
}
@Override
public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
try {
JsonObject jsonObject = json.getAsJsonObject();
OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString();
String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null);
Map<String, Object> update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null;
Map<String, JavaLocalFiles.Local> files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null;
if (osName == null || osArch == null || javaVersion == null)
throw new JsonParseException(json.toString());
return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files);
} catch (JsonParseException e) {
throw e;
} catch (Throwable e) {
throw new JsonParseException(e);
}
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.reflect.TypeToken;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
@@ -55,6 +54,8 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -171,14 +172,11 @@ public final class Accounts {
config().getAccountStorages().setAll(portable);
}
@SuppressWarnings("unchecked")
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll((List<Map<Object, Object>>)
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
}.getType()));
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
} catch (Throwable e) {
LOG.warning("Failed to load global accounts", e);
}
@@ -164,7 +164,7 @@ public final class Config implements Cloneable, Observable {
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@@ -573,15 +573,15 @@ public final class Config implements Cloneable, Observable {
this.launcherFontFamily.set(launcherFontFamily);
}
public int getLogLines() {
public Integer getLogLines() {
return logLines.get();
}
public void setLogLines(int logLines) {
public void setLogLines(Integer logLines) {
this.logLines.set(logLines);
}
public IntegerProperty logLinesProperty() {
public ObjectProperty<Integer> logLinesProperty() {
return logLines;
}
@@ -23,6 +23,8 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
@@ -51,6 +53,10 @@ public final class GlobalConfig implements Cloneable, Observable {
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
private final Map<String, Object> unknownFields = new HashMap<>();
private final transient ObservableHelper helper = new ObservableHelper(this);
@@ -114,11 +120,21 @@ public final class GlobalConfig implements Cloneable, Observable {
this.logRetention.set(logRetention);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
public ObservableSet<String> getDisabledJava() {
return disabledJava;
}
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"platformPromptVersion",
"logRetention"
"logRetention",
"userJava",
"disabledJava"
));
@Override
@@ -131,6 +147,12 @@ public final class GlobalConfig implements Cloneable, Observable {
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
if (!src.getUserJava().isEmpty())
jsonObject.add("userJava", context.serialize(src.getUserJava()));
if (!src.getDisabledJava().isEmpty())
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
}
@@ -149,6 +171,20 @@ public final class GlobalConfig implements Cloneable, Observable {
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
for (JsonElement element : userJava.getAsJsonArray()) {
config.userJava.add(element.getAsString());
}
}
JsonElement disabledJava = obj.get("disabledJava");
if (disabledJava != null && disabledJava.isJsonArray()) {
for (JsonElement element : disabledJava.getAsJsonArray()) {
config.disabledJava.add(element.getAsString());
}
}
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (!knownFields.contains(entry.getKey())) {
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
@@ -0,0 +1,25 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
/**
* @author Glavo
*/
public enum JavaVersionType {
DEFAULT, AUTO, VERSION, DETECTED, CUSTOM
}
@@ -22,29 +22,23 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
@@ -84,38 +78,43 @@ public final class VersionSetting implements Cloneable {
// java
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
private final ObjectProperty<JavaVersionType> javaVersionTypeProperty = new SimpleObjectProperty<>(this, "javaVersionType", JavaVersionType.AUTO);
public StringProperty javaProperty() {
return javaProperty;
public ObjectProperty<JavaVersionType> javaVersionTypeProperty() {
return javaVersionTypeProperty;
}
/**
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
public JavaVersionType getJavaVersionType() {
return javaVersionTypeProperty.get();
}
public void setJava(String java) {
javaProperty.set(java);
public void setJavaVersionType(JavaVersionType javaVersionType) {
javaVersionTypeProperty.set(javaVersionType);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
private final StringProperty javaVersionProperty = new SimpleStringProperty(this, "javaVersion", "");
public StringProperty javaVersionProperty() {
return javaVersionProperty;
}
public String getJavaVersion() {
return javaVersionProperty.get();
}
public void setJavaVersion(String java) {
javaVersionProperty.set(java);
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setJavaVersionType(JavaVersionType.CUSTOM);
setJavaVersion("");
setDefaultJavaPath(null);
}
public boolean isJavaAutoSelected() {
return "Auto".equals(getJava());
}
public void setJavaAutoSelected() {
setJava("Auto");
setJavaVersionType(JavaVersionType.AUTO);
setJavaVersion("");
setDefaultJavaPath(null);
}
@@ -644,58 +643,74 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version) {
return getJavaVersion(gameVersion, version, true);
}
public JavaRuntime getJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
switch (getJavaVersionType()) {
case DEFAULT:
return JavaRuntime.getDefault();
case AUTO:
return JavaManager.findSuitableJava(gameVersion, version);
case CUSTOM:
try {
return JavaManager.getJava(Paths.get(getJavaDir()));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java not found
}
case VERSION: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom");
}
}).thenSupplyAsync(() -> {
try {
if ("Default".equals(getJava())) {
return JavaVersion.fromCurrentEnvironment();
} else if (isJavaAutoSelected()) {
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
} else if (isUsesCustomJavaDir()) {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
FXUtils.runInFX(() -> setJava("Auto"));
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
} catch (InterruptedException e) {
throw new CancellationException();
}
});
}
int majorVersion = -1;
try {
majorVersion = Integer.parseInt(javaVersion);
} catch (NumberFormatException ignored) {
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
if (majorVersion < 0) {
LOG.warning("Invalid Java version: " + javaVersion);
return null;
}
final int finalMajorVersion = majorVersion;
Collection<JavaRuntime> allJava = JavaManager.getAllJava().stream()
.filter(it -> it.getParsedVersion() == finalMajorVersion)
.collect(Collectors.toList());
return JavaManager.findSuitableJava(allJava, gameVersion, version);
}
case DETECTED: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
try {
String defaultJavaPath = getDefaultJavaPath();
if (StringUtils.isNotBlank(defaultJavaPath)) {
JavaRuntime java = JavaManager.getJava(Paths.get(defaultJavaPath).toRealPath());
if (java != null && java.getVersion().equals(javaVersion)) {
return java;
}
}
} catch (IOException | InvalidPathException ignored) {
}
for (JavaRuntime java : JavaManager.getAllJava()) {
if (java.getVersion().equals(javaVersion)) {
return java;
}
}
return null;
}
default:
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
}
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
javaVersionProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
@@ -733,7 +748,8 @@ public final class VersionSetting implements Cloneable {
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setJavaVersionType(getJavaVersionType());
versionSetting.setJavaVersion(getJavaVersion());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
@@ -787,7 +803,6 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
obj.addProperty("postExitCommand", src.getPostExitCommand());
obj.addProperty("serverIp", src.getServerIp());
obj.addProperty("java", src.getJava());
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
@@ -806,6 +821,24 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
obj.addProperty("javaVersionType", src.getJavaVersionType().name());
String java;
switch (src.getJavaVersionType()) {
case DEFAULT:
java = "Default";
break;
case AUTO:
java = "Auto";
break;
case CUSTOM:
java = "Custom";
break;
default:
java = src.getJavaVersion();
break;
}
obj.addProperty("java", java);
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
@@ -846,7 +879,6 @@ public final class VersionSetting implements Cloneable {
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse(""));
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
@@ -865,6 +897,27 @@ public final class VersionSetting implements Cloneable {
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
vs.setJavaVersionType(javaVersionType);
vs.setJavaVersion(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(null));
} else {
String java = Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("");
switch (java) {
case "Default":
vs.setJavaVersionType(JavaVersionType.DEFAULT);
break;
case "Auto":
vs.setJavaVersionType(JavaVersionType.AUTO);
break;
case "Custom":
vs.setJavaVersionType(JavaVersionType.CUSTOM);
break;
default:
vs.setJavaVersion(java);
}
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
@@ -892,5 +945,25 @@ public final class VersionSetting implements Cloneable {
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
}
private <E extends Enum<E>> E parseJsonPrimitive(JsonPrimitive primitive, Class<E> clazz, E defaultValue) {
if (primitive == null)
return defaultValue;
else {
E[] enumConstants = clazz.getEnumConstants();
if (primitive.isNumber()) {
int index = primitive.getAsInt();
return index >= 0 && index < enumConstants.length ? enumConstants[index] : defaultValue;
} else {
String name = primitive.getAsString();
for (E enumConstant : enumConstants) {
if (enumConstant.name().equalsIgnoreCase(name)) {
return enumConstant;
}
}
return defaultValue;
}
}
}
}
}
@@ -37,6 +37,7 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -53,7 +54,6 @@ import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -252,7 +252,7 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).start();
Lang.thread(JavaManager::initialize, "Search Java", true);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
@@ -31,6 +31,7 @@ import javafx.beans.value.WritableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -40,6 +41,7 @@ import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@@ -50,12 +52,12 @@ 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.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
@@ -67,12 +69,9 @@ import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
@@ -81,6 +80,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -408,6 +408,22 @@ public final class FXUtils {
});
}
private static String which(String command) {
String path = System.getenv("PATH");
if (path == null)
return null;
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
try {
Path program = Paths.get(item, command);
if (Files.isExecutable(program))
return program.toRealPath().toString();
} catch (Throwable ignored) {
}
}
return null;
}
public static void showFileInExplorer(Path file) {
String path = file.toAbsolutePath().toString();
@@ -416,6 +432,16 @@ public final class FXUtils {
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
openCommands = new String[]{
"dbus-send",
"--print-reply",
"--dest=org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
"array:string:" + file.toAbsolutePath().toUri(),
"string:"
};
else
openCommands = null;
@@ -472,12 +498,13 @@ public final class FXUtils {
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
String path = which(browser);
if (path != null) {
try {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
} catch (Throwable ignored) {
}
} catch (Throwable ignored) {
}
LOG.warning("No known browser found");
}
@@ -496,52 +523,6 @@ public final class FXUtils {
});
}
public static void showWebDialog(String title, String content) {
showWebDialog(title, content, 800, 480);
}
public static void showWebDialog(String title, String content, int width, int height) {
try {
WebStage stage = new WebStage(width, height);
stage.getWebView().getEngine().loadContent(content);
stage.setTitle(title);
stage.showAndWait();
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.warning("WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtils.initLookAndFeel();
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
final JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
frame.add(progressBar, BorderLayout.PAGE_START);
Schedulers.defaultScheduler().execute(() -> {
final JEditorPane pane = new JEditorPane("text/html", content);
pane.setEditable(false);
pane.addHyperlinkListener(event -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
openLink(event.getURL().toExternalForm());
}
});
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
});
});
frame.setVisible(true);
frame.toFront();
});
}
}
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
@@ -815,6 +796,17 @@ public final class FXUtils {
return button;
}
public static Label truncatedLabel(String text, int limit) {
Label label = new Label();
if (text.length() <= limit) {
label.setText(text);
} else {
label.setText(StringUtils.truncate(text, limit));
installFastTooltip(label, text);
}
return label;
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -977,4 +969,11 @@ public final class FXUtils {
return tf;
}
public static String toWeb(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);
return String.format("#%02x%02x%02x", r, g, b);
}
}
@@ -20,8 +20,6 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -43,6 +41,7 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.Pair;
@@ -77,8 +76,6 @@ public class GameCrashWindow extends Stage {
private final String total_memory;
private final String java;
private final LibraryAnalyzer analyzer;
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final TextFlow feedbackTextFlow = new TextFlow();
@@ -89,9 +86,9 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;
private final Collection<Pair<String, Log4jLevel>> logs;
private final List<Log> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
@@ -124,7 +121,7 @@ public class GameCrashWindow extends Stage {
private void analyzeCrashReport() {
loading.set(true);
Task.allOf(Task.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
// Get the crash-report from the crash-reports/xxx, or the output of console.
String crashReport = null;
@@ -264,20 +261,18 @@ public class GameCrashWindow extends Stage {
private void showLogWindow() {
LogWindow logWindow = new LogWindow(managedProcess);
logWindow.logLine(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
if (managedProcess.getClasspath() != null)
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
logWindow.showNormal();
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
logWindow.logLines(logs);
logWindow.show();
}
private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
CompletableFuture.supplyAsync(() ->
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
@@ -358,12 +353,12 @@ public class GameCrashWindow extends Stage {
TwoLineListItem os = new TwoLineListItem();
os.getStyleClass().setAll("two-line-item-second-large");
os.setTitle(i18n("system.operating_system"));
os.subtitleProperty().bind(GameCrashWindow.this.os);
os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
TwoLineListItem arch = new TwoLineListItem();
arch.getStyleClass().setAll("two-line-item-second-large");
arch.setTitle(i18n("system.architecture"));
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
}
@@ -0,0 +1,266 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Cursor;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HTMLRenderer {
private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
return null;
try {
return new URI(href);
} catch (Throwable e) {
return null;
}
}
private final List<javafx.scene.Node> children = new ArrayList<>();
private final List<Node> stack = new ArrayList<>();
private boolean bold;
private boolean italic;
private boolean underline;
private boolean strike;
private boolean highlight;
private String headerLevel;
private Node hyperlink;
private final Consumer<URI> onClickHyperlink;
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
this.onClickHyperlink = onClickHyperlink;
}
private void updateStyle() {
bold = false;
italic = false;
underline = false;
strike = false;
highlight = false;
headerLevel = null;
hyperlink = null;
for (Node node : stack) {
String nodeName = node.nodeName();
switch (nodeName) {
case "b":
case "strong":
bold = true;
break;
case "i":
case "em":
italic = true;
break;
case "ins":
underline = true;
break;
case "del":
strike = true;
break;
case "mark":
highlight = true;
break;
case "a":
hyperlink = node;
break;
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
headerLevel = nodeName;
break;
}
}
}
private void pushNode(Node node) {
stack.add(node);
updateStyle();
}
private void popNode() {
stack.remove(stack.size() - 1);
updateStyle();
}
private void applyStyle(Text text) {
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
text.setOnMouseClicked(event -> onClickHyperlink.accept(target));
text.setCursor(Cursor.HAND);
}
text.getStyleClass().add("html-hyperlink");
}
if (hyperlink != null || underline)
text.setUnderline(true);
if (strike)
text.setStrikethrough(true);
if (bold || highlight)
text.getStyleClass().add("html-bold");
if (italic)
text.getStyleClass().add("html-italic");
if (headerLevel != null)
text.getStyleClass().add("html-" + headerLevel);
}
private void appendText(String text) {
Text textNode = new Text(text);
applyStyle(textNode);
children.add(textNode);
}
private void appendImage(Node node) {
String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt");
if (imageUri != null) {
URI uri = URI.create(src);
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
double width = 0;
double height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
if (width <= 0 || height <= 0) {
width = 0;
height = 0;
}
}
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
imageView.setOnMouseClicked(event -> onClickHyperlink.accept(target));
imageView.setCursor(Cursor.HAND);
}
}
children.add(imageView);
return;
}
}
if (!alt.isEmpty())
appendText(alt);
}
public void appendNode(Node node) {
if (node instanceof TextNode) {
appendText(((TextNode) node).text());
}
String name = node.nodeName();
switch (name) {
case "img":
appendImage(node);
break;
case "li":
appendText("\n \u2022 ");
break;
case "dt":
appendText(" ");
break;
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
case "tr":
if (!children.isEmpty())
appendText("\n\n");
break;
}
if (node.childNodeSize() > 0) {
pushNode(node);
for (Node childNode : node.childNodes()) {
appendNode(childNode);
}
popNode();
}
switch (name) {
case "br":
case "dd":
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
appendText("\n");
break;
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
}
@@ -25,8 +25,8 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -38,13 +38,14 @@ 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.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
@@ -54,14 +55,11 @@ import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -69,128 +67,120 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public final class LogWindow extends Stage {
private final ArrayDeque<Log> logs = new ArrayDeque<>();
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
private final CircularArrayList<Log> logs;
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
{
for (Log4jLevel level : Log4jLevel.values()) {
levelCountMap.put(level, new SimpleIntegerProperty());
levelShownMap.put(level, new SimpleBooleanProperty(true));
}
};
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) {
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
put(level, property);
}
}
};
private final LogWindowImpl impl = new LogWindowImpl();
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
private boolean stopCheckLogCount = false;
}
private final LogWindowImpl impl;
private final ManagedProcess gameProcess;
public LogWindow(ManagedProcess gameProcess) {
this(gameProcess, new CircularArrayList<>());
}
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
this.logs = logs;
this.impl = new LogWindowImpl();
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
for (SimpleBooleanProperty property : levelShownMap.values()) {
property.addListener(o -> shakeLogs());
}
this.gameProcess = gameProcess;
}
public void logLine(String filteredLine, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(filteredLine), level);
public void logLine(Log log) {
Log4jLevel level = log.getLevel();
logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
if (!stopCheckLogCount) checkLogCount();
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
checkLogCount();
autoScroll();
}
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
this.impl.showCrashReport.set(true);
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
Platform.runLater(() -> {
impl.listView.scrollTo(log);
});
break;
}
public void logLines(List<Log> logs) {
for (Log log : logs) {
Log4jLevel level = log.getLevel();
this.logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
}
show();
}
public void showNormal() {
this.impl.showCrashReport.set(false);
show();
checkLogCount();
autoScroll();
}
private void shakeLogs() {
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList()));
autoScroll();
}
private void checkLogCount() {
while (logs.size() > config().getLogLines()) {
int nRemove = logs.size() - Log.getLogLines();
if (nRemove <= 0)
return;
ObservableList<Log> items = impl.listView.getItems();
int itemsSize = items.size();
int count = 0;
for (int i = 0; i < nRemove; i++) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
impl.listView.getItems().remove(0);
}
if (itemsSize > count && items.get(count) == removedLog)
count++;
}
items.remove(0, count);
}
private static class Log {
private final String log;
private final Log4jLevel level;
private boolean selected = false;
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
private void autoScroll() {
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
}
public class LogWindowImpl extends Control {
private final class LogWindowImpl extends Control {
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
LogWindowImpl() {
getStyleClass().add("log-window");
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000", "10000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
for (int i = 0; i < LEVELS.length; i++) {
buttonText[i] = new SimpleStringProperty();
showLevel[i] = new SimpleBooleanProperty(true);
}
if (!flag)
cboLines.getSelectionModel().select(2);
cboLines.getItems().setAll(500, 2000, 5000, 10000);
cboLines.setValue(Log.getLogLines());
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue));
});
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
for (int i = 0; i < 5; ++i) {
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
levelShownMap.get(levels[i]).bind(showLevel.get(i));
for (int i = 0; i < LEVELS.length; ++i) {
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
}
}
@@ -207,7 +197,7 @@ public final class LogWindow extends Stage {
thread(() -> {
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList()));
} catch (IOException e) {
LOG.warning("Failed to export logs", e);
return;
@@ -223,38 +213,31 @@ public final class LogWindow extends Stage {
});
}
private void onExportDump(JFXButton button) {
private void onExportDump(SpinnerPane pane) {
assert SystemUtils.supportJVMAttachment();
pane.setLoading(true);
thread(() -> {
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
if (SystemUtils.supportJVMAttachment()) {
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
}
});
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
private void onExportGameCrashInfo() {
if (exportGameCrashInfoCallback == null) return;
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> pane.setLoading(false));
});
}
@Override
@@ -263,7 +246,7 @@ public final class LogWindow extends Stage {
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
@@ -275,17 +258,7 @@ public final class LogWindow extends Stage {
private final Set<ListCell<Log>> selected = new HashSet<>();
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + backgroundColor + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(buttonText);
button.setSelected(true);
showLevel.bind(button.selectedProperty());
return button;
}
protected LogWindowSkin(LogWindowImpl control) {
LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
@@ -310,13 +283,16 @@ public final class LogWindow extends Stage {
{
HBox hBox = new HBox(3);
hBox.getChildren().setAll(
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
);
for (int i = 0; i < LEVELS.length; i++) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(control.buttonText[i]);
button.setSelected(true);
control.showLevel[i].bind(button.selectedProperty());
hBox.getChildren().add(button);
}
borderPane.setRight(hBox);
}
@@ -347,11 +323,11 @@ public final class LogWindow extends Stage {
setOnMouseClicked(event -> {
if (!event.isControlDown()) {
for (ListCell<Log> logListCell: selected) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().selected = false;
logListCell.getItem().setSelected(false);
}
}
}
@@ -362,7 +338,7 @@ public final class LogWindow extends Stage {
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().selected = true;
getItem().setSelected(true);
}
});
}
@@ -377,18 +353,18 @@ public final class LogWindow extends Stage {
lastCell.value = this;
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.selected);
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
if (empty) {
setText(null);
} else {
setText(item.log);
setText(item.getLog());
}
}
});
@@ -398,10 +374,9 @@ public final class LogWindow extends Stage {
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.selected) {
if (item.log != null) {
stringBuilder.append(item.log);
}
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
}
}
@@ -417,11 +392,6 @@ public final class LogWindow extends Stage {
{
BorderPane bottom = new BorderPane();
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
bottom.setLeft(exportGameCrashInfoButton);
HBox hBox = new HBox(3);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
@@ -432,24 +402,24 @@ public final class LogWindow extends Stage {
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
JFXButton exportDumpButton = new JFXButton();
SpinnerPane exportDumpPane = new SpinnerPane();
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
exportDumpButton.setDisable(true);
}
exportDumpPane.setContent(exportDumpButton);
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
vbox.getChildren().add(bottom);
}
@@ -110,7 +110,8 @@ public enum SVG {
LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"),
THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"),
THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"),
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z");
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"),
JAVA("m 12.400746,23.498132 c 0,0 -1.047682,0.609185 0.745753,0.816159 2.173248,0.247515 3.284943,0.212301 5.680103,-0.241112 0,0 0.629462,0.394742 1.509642,0.737207 -5.369636,2.301245 -12.1518319,-0.133351 -7.934431,-1.312254 z m -0.656135,-3.003246 c 0,0 -1.175708,0.870569 0.619861,1.056206 2.321545,0.238977 4.155521,0.25925 7.32844,-0.35207 0,0 0.43849,0.444887 1.128765,0.688135 -6.492002,1.897965 -13.7233551,0.149351 -9.077066,-1.392271 z m 5.531806,-5.094318 c 1.322937,1.523496 -0.347807,2.894427 -0.347807,2.894427 0,0 3.359626,-1.733668 1.816908,-3.905821 -1.441364,-2.024926 -2.545591,-3.030987 3.43644,-6.5004569 0,0 -9.389665,2.3449859 -4.905541,7.5129179 z m 7.101193,10.318794 c 0,0 0.775626,0.639057 -0.854578,1.133019 C 20.42373,27.79123 10.623314,28.075018 7.9006204,26.88973 6.9222859,26.464048 8.75733,25.873001 9.3345153,25.749244 9.9362392,25.619093 10.280844,25.642568 10.280844,25.642568 9.1926195,24.875486 3.2457961,27.147927 7.2604887,27.798718 18.208875,29.573994 27.21766,26.999632 24.37761,25.718316 Z M 12.904316,17.383886 c 0,0 -4.9855587,1.184229 -1.765696,1.614178 1.359213,0.182426 4.069103,0.140825 6.594422,-0.07042 2.063359,-0.173899 4.135252,-0.544104 4.135252,-0.544104 0,0 -0.727616,0.311527 -1.253592,0.671064 -5.062375,1.331456 -14.8414526,0.711604 -12.02594,-0.649727 2.38129,-1.151156 4.316621,-1.019929 4.316621,-1.019929 z m 8.943706,4.998298 c 5.146658,-2.673583 2.766435,-5.24368 1.106361,-4.898013 -0.407551,0.08428 -0.58892,0.1579 -0.58892,0.1579 0,0 0.151501,-0.236845 0.439557,-0.339265 3.284942,-1.155424 5.812393,3.406524 -1.060485,5.213807 0,0 0.08003,-0.07147 0.103478,-0.134425 z M 18.744451,2.2855 c 0,0 2.849653,2.8506848 -2.703489,7.2344656 -4.453183,3.5164124 -1.015676,5.5221324 -0.0021,7.8127104 C 13.439893,14.987688 11.532301,12.92329 12.811497,11.001852 14.690284,8.1810394 19.893487,6.8143757 18.743384,2.2855 Z M 13.41002,29.628384 c 4.939684,0.315794 12.525242,-0.174974 12.70448,-2.512485 0,0 -0.345672,0.886571 -4.081906,1.589641 -4.216334,0.793754 -9.416337,0.700936 -12.4996379,0.192025 0,0 0.6315969,0.522768 3.8781309,0.730807 z");
private final String path;
@@ -19,58 +19,73 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.concurrent.Worker;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import java.io.IOException;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class UpgradeDialog extends JFXDialogLayout {
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
try {
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(url);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.FAILED) {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
});
setBody(webView);
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.warning("WebView is missing or initialization failed", e);
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
if (node == null)
throw new IOException("Cannot find #nowchange in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
FXUtils.openLink(uri.toString());
});
do {
renderer.appendNode(node);
node = node.nextSibling();
} while (node != null);
return renderer.render();
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane(result);
scrollPane.setFitToWidth(true);
setBody(scrollPane);
} else {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
}
}).start();
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
openInBrowser.setExternalLink(url);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnAction(e -> updateRunnable.run());
setActions(updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
setActions(openInBrowser, updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
}
@@ -0,0 +1,75 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WebPage extends SpinnerPane implements DecoratorPage {
private final ObjectProperty<DecoratorPage.State> stateProperty;
public WebPage(String title, String content) {
this.stateProperty = new SimpleObjectProperty<>(DecoratorPage.State.fromTitle(title));
this.setBackground(new Background(new BackgroundFill(Color.WHITE, null, null)));
Task.supplyAsync(() -> {
Document document = Jsoup.parseBodyFragment(content);
HTMLRenderer renderer = new HTMLRenderer(uri -> {
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
FXUtils.openLink(uri.toString());
}, null);
});
renderer.appendNode(document);
return renderer.render();
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setContent(result);
setContent(scrollPane);
setFailedReason(null);
} else {
LOG.warning("Failed to load content", exception);
setFailedReason(i18n("web.failed"));
}
})).start();
}
@Override
public ReadOnlyObjectProperty<DecoratorPage.State> stateProperty() {
return stateProperty;
}
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class WebStage extends Stage {
protected final StackPane pane = new StackPane();
protected final JFXProgressBar progressBar = new JFXProgressBar();
protected final WebView webView = new WebView();
protected final WebEngine webEngine = webView.getEngine();
public WebStage() {
this(800, 480);
}
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
FXUtils.setIcon(this);
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
switch (webView.getEngine().getLoadWorker().getState()) {
case SUCCEEDED:
case FAILED:
case CANCELLED:
return false;
default:
return true;
}
}, webEngine.getLoadWorker().stateProperty()));
BorderPane borderPane = new BorderPane();
borderPane.setPickOnBounds(false);
borderPane.setTop(progressBar);
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
pane.getChildren().setAll(webView, borderPane);
}
public WebView getWebView() {
return webView;
}
}
@@ -177,7 +177,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
right.getChildren().add(spinnerCopyUUID);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.setOnMouseClicked(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1, -1));
@@ -123,7 +123,9 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
@@ -166,6 +166,16 @@ public final class MessageDialogPane extends HBox {
return this;
}
public Builder addAction(String text, @Nullable Runnable action) {
JFXButton btnAction = new JFXButton(text);
btnAction.getStyleClass().add("dialog-accept");
if (action != null) {
btnAction.setOnAction(e -> action.run());
}
dialog.addButton(btnAction);
return this;
}
public Builder ok(@Nullable Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
@@ -57,7 +57,7 @@ public final class MultiFileItem<T> extends VBox {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
selectedData.set((T) newValue.getUserData());
selectedData.set(newValue != null ? (T) newValue.getUserData() : null);
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
@@ -183,6 +183,10 @@ public final class MultiFileItem<T> extends VBox {
super(title, data);
}
public JFXTextField getCustomField() {
return customField;
}
public String getValue() {
return customField.getText();
}
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.java.JavaInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
@@ -159,8 +160,10 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
} else if (task instanceof JavaDownloadTask) {
} else if (task instanceof MojangJavaDownloadTask) {
task.setName(i18n("download.java"));
} else if (task instanceof JavaInstallTask) {
task.setName(i18n("java.install"));
}
Platform.runLater(() -> {
@@ -52,6 +52,8 @@ public class InstallersPage extends Control implements WizardPage {
protected JFXTextField txtName = new JFXTextField();
protected BooleanProperty installable = new SimpleBooleanProperty();
private boolean isNameModifiedByUser = false;
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
this.controller = controller;
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
@@ -61,7 +63,8 @@ public class InstallersPage extends Control implements WizardPage {
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
txtName.setText(gameVersion);
txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true);
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
@@ -103,6 +106,9 @@ public class InstallersPage extends Control implements WizardPage {
library.versionProperty().set(null);
}
}
if (!isNameModifiedByUser) {
setTxtNameWithLoaders();
}
}
@Override
@@ -124,6 +130,49 @@ public class InstallersPage extends Control implements WizardPage {
return new InstallersPageSkin(this);
}
private void setTxtNameWithLoaders() {
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().getVersion());
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
if (!controller.getSettings().containsKey(libraryId)) {
continue;
}
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
if (libraryType != null) {
String loaderName;
switch (libraryType) {
case FORGE:
loaderName = i18n("install.installer.forge");
break;
case NEO_FORGE:
loaderName = i18n("install.installer.neoforge");
break;
case FABRIC:
loaderName = i18n("install.installer.fabric");
break;
case LITELOADER:
loaderName = i18n("install.installer.liteloader");
break;
case QUILT:
loaderName = i18n("install.installer.quilt");
break;
case OPTIFINE:
loaderName = i18n("install.installer.optifine");
break;
default:
continue;
}
nameBuilder.append("-").append(loaderName);
}
}
txtName.setText(nameBuilder.toString());
isNameModifiedByUser = false;
}
protected static class InstallersPageSkin extends SkinBase<InstallersPage> {
/**
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -107,7 +108,6 @@ public final class LocalModpackPage extends ModpackPage {
hideSpinner();
lblName.setText(selectedFile.getName());
installAsVersion.set(false);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
@@ -130,8 +130,6 @@ public final class LocalModpackPage extends ModpackPage {
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
@@ -153,9 +151,8 @@ public final class LocalModpackPage extends ModpackPage {
}
protected void onDescribe() {
if (manifest != null) {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
if (manifest != null)
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_FILE = "MODPACK_FILE";
@@ -22,7 +22,6 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final Label lblName;
protected final Label lblVersion;
protected final Label lblAuthor;
protected final Label lblModpackLocation;
protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall;
@@ -37,15 +36,6 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
ComponentList componentList = new ComponentList();
{
BorderPane locationPane = new BorderPane();
{
locationPane.setLeft(new Label(i18n("modpack.task.install.will")));
lblModpackLocation = new Label();
BorderPane.setAlignment(lblModpackLocation, Pos.CENTER_RIGHT);
locationPane.setCenter(lblModpackLocation);
}
BorderPane archiveNamePane = new BorderPane();
{
Label label = new Label(i18n("archive.file.name"));
@@ -99,7 +89,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
}
componentList.getContent().setAll(
locationPane, archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
}
borderPane.getChildren().setAll(componentList);
@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -43,7 +43,6 @@ public final class RemoteModpackPage extends ModpackPage {
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
lblModpackLocation.setText(manifest.getFileApi());
try {
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack(null));
@@ -84,7 +83,7 @@ public final class RemoteModpackPage extends ModpackPage {
}
protected void onDescribe() {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
@@ -17,18 +17,30 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class AboutPage extends StackPane {
public final class AboutPage extends StackPane {
public AboutPage() {
ComponentList about = new ComponentList();
@@ -48,131 +60,9 @@ public class AboutPage extends StackPane {
about.getContent().setAll(launcher, author);
}
ComponentList thanks = new ComponentList();
{
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
yushijinhun.setImage(FXUtils.newBuiltinImage("/assets/img/yushijinhun.png"));
yushijinhun.setTitle("yushijinhun");
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
yushijinhun.setExternalLink("https://yushi.moe/");
ComponentList thanks = loadIconedTwoLineList("/assets/about/thanks.json");
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
bangbang93.setImage(FXUtils.newBuiltinImage("/assets/img/bangbang93.png"));
bangbang93.setTitle("bangbang93");
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
glavo.setImage(FXUtils.newBuiltinImage("/assets/img/glavo.png"));
glavo.setTitle("Glavo");
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
glavo.setExternalLink("https://github.com/Glavo");
IconedTwoLineListItem zekerzhayard = new IconedTwoLineListItem();
zekerzhayard.setImage(FXUtils.newBuiltinImage("/assets/img/zekerzhayard.png"));
zekerzhayard.setTitle("ZekerZhayard");
zekerzhayard.setSubtitle(i18n("about.thanks_to.zekerzhayard.statement"));
zekerzhayard.setExternalLink("https://github.com/ZekerZhayard");
IconedTwoLineListItem zkitefly = new IconedTwoLineListItem();
zkitefly.setImage(FXUtils.newBuiltinImage("/assets/img/zkitefly.png"));
zkitefly.setTitle("Zkitefly");
zkitefly.setSubtitle(i18n("about.thanks_to.zkitefly.statement"));
zkitefly.setExternalLink("https://github.com/zkitefly");
IconedTwoLineListItem burningtnt = new IconedTwoLineListItem();
burningtnt.setImage(FXUtils.newBuiltinImage("/assets/img/burningtnt.png"));
burningtnt.setTitle("Burning_TNT");
burningtnt.setSubtitle(i18n("about.thanks_to.burningtnt.statement"));
burningtnt.setExternalLink("https://github.com/burningtnt");
IconedTwoLineListItem shulkerSakura = new IconedTwoLineListItem();
shulkerSakura.setTitle("ShulkerSakura");
shulkerSakura.setImage(FXUtils.newBuiltinImage("/assets/img/ShulkerSakura.png"));
shulkerSakura.setSubtitle(i18n("about.thanks_to.shulkersakura.statement"));
shulkerSakura.setExternalLink("https://github.com/ShulkerSakura");
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
gamerteam.setTitle("gamerteam");
gamerteam.setImage(FXUtils.newBuiltinImage("/assets/img/gamerteam.png"));
gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
gamerteam.setExternalLink("http://www.zhaisoul.com/");
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
redLnn.setTitle("Red_lnn");
redLnn.setImage(FXUtils.newBuiltinImage("/assets/img/red_lnn.png"));
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
IconedTwoLineListItem mcmod = new IconedTwoLineListItem();
mcmod.setImage(FXUtils.newBuiltinImage("/assets/img/mcmod.png"));
mcmod.setTitle(i18n("about.thanks_to.mcmod"));
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
mcbbs.setImage(FXUtils.newBuiltinImage("/assets/img/chest.png"));
mcbbs.setTitle(i18n("about.thanks_to.mcbbs"));
mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement"));
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(FXUtils.newBuiltinImage("/assets/img/github.png"));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/HMCL-dev/HMCL/graphs/contributors");
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
users.setTitle(i18n("about.thanks_to.users"));
users.setSubtitle(i18n("about.thanks_to.users.statement"));
users.setExternalLink("https://docs.hmcl.net/groups.html");
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, zekerzhayard, zkitefly, burningtnt, mcmod, mcbbs, shulkerSakura, gamerteam, redLnn, contributors, users);
}
ComponentList dep = new ComponentList();
{
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
javafx.setTitle("JavaFX");
javafx.setSubtitle("Copyright © 2013, 2024, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setExternalLink("https://openjfx.io/");
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
jfoenix.setTitle("JFoenix");
jfoenix.setSubtitle("Copyright © 2016 JFoenix.\nLicensed under the MIT License.");
jfoenix.setExternalLink("https://github.com/sshahine/JFoenix");
IconedTwoLineListItem gson = new IconedTwoLineListItem();
gson.setTitle("Gson");
gson.setSubtitle("Copyright © 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
gson.setExternalLink("https://github.com/google/gson");
IconedTwoLineListItem xz = new IconedTwoLineListItem();
xz.setTitle("XZ for Java");
xz.setSubtitle("Lasse Collin, Igor Pavlov, and/or Brett Okken.\nPublic Domain.");
xz.setExternalLink("https://tukaani.org/xz/java.html");
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
fxgson.setTitle("fx-gson");
fxgson.setSubtitle("Copyright © 2016 Joffrey Bion.\nLicensed under the MIT License.");
fxgson.setExternalLink("https://github.com/joffrey-bion/fx-gson");
IconedTwoLineListItem constantPoolScanner = new IconedTwoLineListItem();
constantPoolScanner.setTitle("Constant Pool Scanner");
constantPoolScanner.setSubtitle("Copyright © 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
constantPoolScanner.setExternalLink("https://github.com/jenkinsci/constant-pool-scanner");
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
openNBT.setTitle("OpenNBT");
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/GeyserMC/OpenNBT");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
minecraftJFXSkin.setSubtitle("Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.");
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, minecraftJFXSkin);
}
ComponentList deps = loadIconedTwoLineList("/assets/about/deps.json");
ComponentList legal = new ComponentList();
{
@@ -204,7 +94,7 @@ public class AboutPage extends StackPane {
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
dep,
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
@@ -216,4 +106,49 @@ public class AboutPage extends StackPane {
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static ComponentList loadIconedTwoLineList(String path) {
ComponentList componentList = new ComponentList();
InputStream input = FXUtils.class.getResourceAsStream(path);
if (input == null) {
LOG.warning("Resources not found: " + path);
return componentList;
}
try (Reader reader = new InputStreamReader(input)) {
JsonArray array = JsonUtils.GSON.fromJson(reader, JsonArray.class);
for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject();
IconedTwoLineListItem item = new IconedTwoLineListItem();
if (obj.has("image")) {
String image = obj.get("image").getAsString();
item.setImage(image.startsWith("/")
? FXUtils.newBuiltinImage(image)
: new Image(image));
}
if (obj.has("title"))
item.setTitle(obj.get("title").getAsString());
else if (obj.has("titleLocalized"))
item.setTitle(i18n(obj.get("titleLocalized").getAsString()));
if (obj.has("subtitle"))
item.setSubtitle(obj.get("subtitle").getAsString());
else if (obj.has("subtitleLocalized"))
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
if (obj.has("externalLink"))
item.setExternalLink(obj.get("externalLink").getAsString());
componentList.getContent().add(item);
}
} catch (IOException | JsonParseException e) {
LOG.warning("Failed to load list: " + path, e);
}
return componentList;
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
@@ -34,6 +33,7 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class HelpPage extends SpinnerPane {
@@ -63,8 +63,7 @@ public class HelpPage extends SpinnerPane {
private void loadHelp() {
showSpinner();
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(new TypeToken<List<HelpCategory>>() {
}.getType()))
Task.supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(listTypeOf(HelpCategory.class)))
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList();
@@ -0,0 +1,423 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.download.java.disco.*;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaDownloadDialog extends StackPane {
public static Runnable showDialogAction(DownloadProvider downloadProvider) {
Platform platform = Platform.SYSTEM_PLATFORM;
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(platform);
EnumSet<DiscoJavaDistribution> distributions = EnumSet.noneOf(DiscoJavaDistribution.class);
for (DiscoJavaDistribution distribution : DiscoJavaDistribution.values()) {
if (distribution.isSupport(platform)) {
distributions.add(distribution);
}
}
return supportedVersions.isEmpty() && distributions.isEmpty()
? null
: () -> Controllers.dialog(new JavaDownloadDialog(downloadProvider, platform, supportedVersions, distributions));
}
private final DownloadProvider downloadProvider;
private final Platform platform;
private final List<GameJavaVersion> supportedGameJavaVersions;
private final EnumSet<DiscoJavaDistribution> distributions;
private JavaDownloadDialog(DownloadProvider downloadProvider, Platform platform, List<GameJavaVersion> supportedGameJavaVersions, EnumSet<DiscoJavaDistribution> distributions) {
this.downloadProvider = downloadProvider;
this.platform = platform;
this.supportedGameJavaVersions = supportedGameJavaVersions;
this.distributions = distributions;
if (!supportedGameJavaVersions.isEmpty()) {
this.getChildren().add(new DownloadMojangJava());
} else {
this.getChildren().add(new DownloadDiscoJava());
}
}
private final class DownloadMojangJava extends DialogPane {
private final ToggleGroup toggleGroup = new ToggleGroup();
DownloadMojangJava() {
setTitle(i18n("java.download"));
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
VBox vbox = new VBox(16);
Label prompt = new Label(i18n("java.download.prompt"));
vbox.getChildren().add(prompt);
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXRadioButton button = new JFXRadioButton("Java " + version.getMajorVersion());
button.setUserData(version);
vbox.getChildren().add(button);
toggleGroup.getToggles().add(button);
}
setBody(vbox);
if (!distributions.isEmpty()) {
JFXHyperlink more = new JFXHyperlink(i18n("java.download.more"));
more.setOnAction(event -> JavaDownloadDialog.this.getChildren().setAll(new DownloadDiscoJava()));
setActions(warningLabel, more, acceptPane, cancelButton);
} else
setActions(warningLabel, acceptPane, cancelButton);
}
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
});
}
@Override
protected void onAccept() {
fireEvent(new DialogCloseEvent());
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
if (javaVersion == null)
return;
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
Controllers.confirm(i18n("download.java.override"), null, () -> {
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
.thenComposeAsync(Schedulers.javafx(), realPath -> {
if (realPath != null) {
JavaManager.removeJava(realPath);
}
return downloadTask(javaVersion);
}), i18n("download.java"), TaskCancellationAction.NORMAL);
}, null);
else
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java"), TaskCancellationAction.NORMAL);
}
}
private final class DownloadDiscoJava extends JFXDialogLayout {
private boolean isLTS(int major) {
if (major <= 8) {
return true;
}
if (major < 21) {
return major == 11 || major == 17;
}
return major % 4 == 1;
}
private final JFXComboBox<DiscoJavaDistribution> distributionBox;
private final JFXComboBox<DiscoJavaRemoteVersion> remoteVersionBox;
private final JFXComboBox<JavaPackageType> packageTypeBox;
private final Label warningLabel = new Label();
private final JFXButton downloadButton;
private final StackPane downloadButtonPane = new StackPane();
private final DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
private final ObjectProperty<DiscoJavaVersionList> currentDiscoJavaVersionList = new SimpleObjectProperty<>();
private final Map<Pair<DiscoJavaDistribution, JavaPackageType>, DiscoJavaVersionList> javaVersionLists = new HashMap<>();
private boolean changingDistribution = false;
DownloadDiscoJava() {
assert !distributions.isEmpty();
this.distributionBox = new JFXComboBox<>();
this.distributionBox.setConverter(FXUtils.stringConverter(JavaDistribution::getDisplayName));
this.remoteVersionBox = new JFXComboBox<>();
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
this.packageTypeBox = new JFXComboBox<>();
this.downloadButton = new JFXButton(i18n("download"));
downloadButton.setOnAction(e -> onDownload());
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty()));
downloadButtonPane.getChildren().setAll(downloadButton);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
cancelButton.getStyleClass().add("dialog-cancel");
onEscPressed(this, cancelButton::fire);
GridPane body = new GridPane();
body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
body.setVgap(8);
body.setHgap(16);
body.addRow(0, new Label(i18n("java.download.distribution")), distributionBox);
body.addRow(1, new Label(i18n("java.download.version")), remoteVersionBox);
body.addRow(2, new Label(i18n("java.download.packageType")), packageTypeBox);
distributionBox.setItems(FXCollections.observableList(new ArrayList<>(distributions)));
ChangeListener<DiscoJavaVersionList.Status> updateStatusListener = (observable, oldValue, newValue) -> updateStatus(newValue);
this.currentDiscoJavaVersionList.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.status.removeListener(updateStatusListener);
}
if (newValue != null) {
newValue.status.addListener(updateStatusListener);
updateStatus(newValue.status.get());
} else {
updateStatus(null);
}
});
packageTypeBox.getSelectionModel().selectedItemProperty().addListener(ignored -> updateVersions());
FXUtils.onChangeAndOperate(distributionBox.getSelectionModel().selectedItemProperty(), distribution -> {
if (distribution != null) {
changingDistribution = true;
packageTypeBox.setItems(FXCollections.observableList(new ArrayList<>(distribution.getSupportedPackageTypes())));
packageTypeBox.getSelectionModel().select(0);
changingDistribution = false;
updateVersions();
packageTypeBox.setDisable(false);
remoteVersionBox.setDisable(false);
} else {
packageTypeBox.setItems(null);
updateVersions();
remoteVersionBox.setItems(null);
packageTypeBox.setDisable(true);
remoteVersionBox.setDisable(true);
}
});
setHeading(new Label(i18n("java.download")));
setBody(body);
setActions(warningLabel, downloadButtonPane, cancelButton);
}
private void updateStatus(DiscoJavaVersionList.Status status) {
if (status == DiscoJavaVersionList.Status.LOADING) {
downloadButtonPane.getChildren().setAll(new JFXSpinner());
remoteVersionBox.setDisable(true);
warningLabel.setText(null);
} else {
downloadButtonPane.getChildren().setAll(downloadButton);
if (status == DiscoJavaVersionList.Status.SUCCESS || status == null) {
remoteVersionBox.setDisable(false);
warningLabel.setText(null);
} else if (status == DiscoJavaVersionList.Status.FAILED) {
remoteVersionBox.setDisable(true);
warningLabel.setText(i18n("java.download.load_list.failed"));
}
}
}
private void onDownload() {
fireEvent(new DialogCloseEvent());
DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem();
DiscoJavaRemoteVersion version = remoteVersionBox.getSelectionModel().getSelectedItem();
if (version == null)
return;
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri()))
.setExecutor(Schedulers.io())
.thenComposeAsync(json -> {
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
if (result.getResult().size() != 1)
throw new IOException("Illegal result: " + json);
DiscoRemoteFileInfo fileInfo = result.getResult().get(0);
if (!fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256"))
throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType());
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
throw new IOException("Missing download URI: " + json);
File targetFile = File.createTempFile("hmcl-java-", "." + version.getArchiveType());
targetFile.deleteOnExit();
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum()));
else if (StringUtils.isNotBlank(fileInfo.getChecksumUri()))
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri()))
.thenApplyAsync(checksum ->
new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum.trim()));
else
throw new IOException("Unable to get checksum for file");
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
String javaVersion = version.getJavaVersion();
JavaInfo info = new JavaInfo(platform, javaVersion, distribution.getVendor());
Map<String, Object> updateInfo = new LinkedHashMap<>();
updateInfo.put("type", "disco");
updateInfo.put("info", version);
int idx = javaVersion.indexOf('+');
if (idx > 0) {
javaVersion = javaVersion.substring(0, idx);
}
String defaultName = distribution.getApiParameter() + "-" + javaVersion;
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller ->
new JavaInstallPage(controller::onFinish, info, version, updateInfo, defaultName, result)));
} else {
LOG.warning("Failed to download java", exception);
Throwable resolvedException = resolveException(exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
})), i18n("java.download"), TaskCancellationAction.NORMAL);
}
private void updateVersions() {
if (changingDistribution) return;
DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem();
if (distribution == null) {
this.currentDiscoJavaVersionList.set(null);
return;
}
JavaPackageType packageType = packageTypeBox.getSelectionModel().getSelectedItem();
DiscoJavaVersionList list = javaVersionLists.computeIfAbsent(Pair.pair(distribution, packageType), pair -> {
DiscoJavaVersionList res = new DiscoJavaVersionList();
new DiscoFetchJavaListTask(downloadProvider, distribution, platform, packageType).setExecutor(Schedulers.io()).thenApplyAsync(versions -> {
if (versions.isEmpty()) return Collections.<DiscoJavaRemoteVersion>emptyList();
int lastLTS = -1;
for (int v : versions.keySet()) {
if (isLTS(v)) {
lastLTS = v;
}
}
ArrayList<DiscoJavaRemoteVersion> remoteVersions = new ArrayList<>();
for (Map.Entry<Integer, DiscoJavaRemoteVersion> entry : versions.entrySet()) {
int v = entry.getKey();
if (v >= lastLTS || isLTS(v) || v == 16) {
remoteVersions.add(entry.getValue());
}
}
Collections.reverse(remoteVersions);
return remoteVersions;
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
res.status.set(DiscoJavaVersionList.Status.SUCCESS);
res.versions.setAll(result);
selectLTS(res);
} else {
LOG.warning("Failed to load java list", exception);
res.status.set(DiscoJavaVersionList.Status.FAILED);
}
})).start();
return res;
});
this.currentDiscoJavaVersionList.set(list);
this.remoteVersionBox.setItems(list.versions);
selectLTS(list);
}
private void selectLTS(DiscoJavaVersionList list) {
if (remoteVersionBox.getItems() == list.versions) {
for (int i = 0; i < list.versions.size(); i++) {
JavaRemoteVersion item = list.versions.get(i);
if (item.getJdkVersion() == GameJavaVersion.LATEST.getMajorVersion()) {
remoteVersionBox.getSelectionModel().select(i);
break;
}
}
}
}
}
private static final class DiscoJavaVersionList {
enum Status {
LOADING, SUCCESS, FAILED
}
final ObservableList<DiscoJavaRemoteVersion> versions = FXCollections.observableArrayList();
final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
}
}
@@ -0,0 +1,190 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.download.java.disco.DiscoJavaDistribution;
import org.jackhuang.hmcl.download.java.disco.DiscoJavaRemoteVersion;
import org.jackhuang.hmcl.java.HMCLJavaRepository;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class JavaInstallPage extends WizardSinglePage {
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9.\\-_]+");
private final Path file;
private final JavaInfo info;
private final JavaRemoteVersion remoteVersion;
private final Map<String, Object> update;
private final StringProperty nameProperty = new SimpleStringProperty();
public JavaInstallPage(Runnable onFinish, JavaInfo info, JavaRemoteVersion remoteVersion, Map<String, Object> update, String defaultName, Path file) {
super(onFinish);
this.info = info;
this.remoteVersion = remoteVersion;
this.update = update;
this.file = file;
this.nameProperty.set(defaultName);
}
@Override
protected SkinBase<?> createDefaultSkin() {
return new Skin(this);
}
@Override
protected Object finish() {
Task<JavaRuntime> installTask = JavaManager.getInstallJavaTask(info.getPlatform(), nameProperty.get(), update, file);
return remoteVersion == null ? installTask : installTask.whenComplete(exception -> {
try {
Files.delete(file);
} catch (IOException e) {
LOG.warning("Failed to delete file: " + file, e);
}
});
}
@Override
public String getTitle() {
return i18n("java.install");
}
private static final class Skin extends SkinBase<JavaInstallPage> {
private final ComponentList componentList = new ComponentList();
private final JFXTextField nameField;
private final Set<String> usedNames = new HashSet<>();
Skin(JavaInstallPage control) {
super(control);
VBox borderPane = new VBox();
borderPane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(borderPane, 500);
{
BorderPane namePane = new BorderPane();
{
Label label = new Label(i18n("java.install.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
namePane.setLeft(label);
nameField = new JFXTextField();
nameField.textProperty().bindBidirectional(control.nameProperty);
FXUtils.setLimitWidth(nameField, 200);
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0));
namePane.setRight(nameField);
nameField.setValidators(
new RequiredValidator(),
new Validator(i18n("java.install.warning.invalid_character"),
text -> !text.startsWith(HMCLJavaRepository.MOJANG_JAVA_PREFIX) && NAME_PATTERN.matcher(text).matches()),
new Validator(i18n("java.install.failed.exists"), text -> !usedNames.contains(text))
);
String defaultName = control.nameProperty.get();
if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), defaultName)) {
usedNames.add(defaultName);
}
nameField.textProperty().addListener(o -> nameField.validate());
nameField.validate();
componentList.getContent().add(namePane);
}
String vendor = JavaInfo.normalizeVendor(control.info.getVendor());
if (vendor != null)
addInfo(i18n("java.info.vendor"), vendor);
if (control.remoteVersion instanceof DiscoJavaRemoteVersion) {
String distributionName = ((DiscoJavaRemoteVersion) control.remoteVersion).getDistribution();
DiscoJavaDistribution distribution = DiscoJavaDistribution.of(distributionName);
addInfo(i18n("java.info.disco.distribution"), distribution != null ? distribution.getDisplayName() : distributionName);
} else
addInfo(i18n("java.install.archive"), control.file.toAbsolutePath().toString());
addInfo(i18n("java.info.version"), control.info.getVersion());
addInfo(i18n("java.info.architecture"), control.info.getPlatform().getArchitecture().getDisplayName());
BorderPane installPane = new BorderPane();
{
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
installButton.setOnAction(e -> {
String name = control.nameProperty.get();
if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), name)) {
Controllers.dialog(i18n("java.install.failed.exists"), null, MessageDialogPane.MessageType.WARNING);
usedNames.add(name);
nameField.validate();
} else
control.onFinish.run();
});
installButton.disableProperty().bind(nameField.activeValidatorProperty().isNotNull());
installPane.setRight(installButton);
componentList.getContent().add(installPane);
}
}
borderPane.getChildren().setAll(componentList);
this.getChildren().setAll(borderPane);
}
private void addInfo(String name, String value) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(name));
Label valueLabel = FXUtils.truncatedLabel(value, 60);
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
pane.setCenter(valueLabel);
this.componentList.getContent().add(pane);
}
}
}
@@ -0,0 +1,322 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManagementPage extends ListPageBase<JavaManagementPage.JavaItem> {
@SuppressWarnings("FieldCanBeLocal")
private final ChangeListener<Collection<JavaRuntime>> listener;
private final Runnable onInstallJava;
public JavaManagementPage() {
this.listener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), this::loadJava);
if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW)) {
onInstallJava = () -> FXUtils.openLink("https://www.loongnix.cn/zh/api/java/");
} else {
onInstallJava = JavaDownloadDialog.showDialogAction(DownloadProviders.getDownloadProvider());
}
FXUtils.applyDragListener(this, it -> {
String name = it.getName();
return it.isDirectory() || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
}, files -> {
for (File file : files) {
if (file.isDirectory()) {
onAddJavaHome(file.toPath());
} else {
String fileName = file.getName();
if (fileName.equals(OperatingSystem.CURRENT_OS.getJavaExecutable())) {
onAddJavaBinary(file.toPath());
} else if (fileName.endsWith(".zip") || fileName.endsWith(".tar.gz")) {
onInstallArchive(file.toPath());
} else {
throw new AssertionError("Unreachable code");
}
}
}
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaPageSkin(this);
}
void onAddJava() {
FileChooser chooser = new FileChooser();
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
chooser.setTitle(i18n("settings.game.java_directory.choose"));
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null) {
JavaManager.getAddJavaTask(file.toPath()).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
}
void onShowRestoreJavaPage() {
Controllers.navigate(new JavaRestorePage(ConfigHolder.globalConfig().getDisabledJava()));
}
private void onAddJavaBinary(Path file) {
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void onAddJavaHome(Path file) {
Task.composeAsync(() -> {
Path releaseFile = file.resolve("release");
if (Files.notExists(releaseFile))
throw new IOException("Missing release file " + releaseFile);
return JavaManager.getAddJavaTask(file.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable()));
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void onInstallArchive(Path file) {
Task.supplyAsync(() -> {
try (ArchiveFileTree<?, ?> tree = TarFileTree.open(file)) {
JavaInfo info = JavaInfo.fromArchive(tree);
if (!JavaManager.isCompatible(info.getPlatform()))
throw new UnsupportedPlatformException(info.getPlatform().toString());
return Pair.pair(tree.getRoot().getSubDirs().keySet().iterator().next(), info);
}
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller ->
new JavaInstallPage(controller::onFinish, result.getValue(), null, null, result.getKey(), file)));
} else {
if (exception instanceof UnsupportedPlatformException) {
Controllers.dialog(i18n("java.install.failed.unsupported_platform"), null, MessageDialogPane.MessageType.WARNING);
} else {
Controllers.dialog(i18n("java.install.failed.invalid"), null, MessageDialogPane.MessageType.WARNING);
}
}
}).start();
}
// FXThread
private void loadJava(Collection<JavaRuntime> javaRuntimes) {
if (javaRuntimes != null) {
List<JavaItem> items = new ArrayList<>();
for (JavaRuntime java : javaRuntimes) {
items.add(new JavaItem(java));
}
this.setItems(FXCollections.observableList(items));
this.setLoading(false);
} else
this.setLoading(true);
}
static final class JavaItem extends Control {
private final JavaRuntime java;
public JavaItem(JavaRuntime java) {
this.java = java;
}
public JavaRuntime getJava() {
return java;
}
public void onReveal() {
Path target;
Path parent = java.getBinary().getParent();
if (parent != null
&& parent.getParent() != null
&& parent.getFileName() != null
&& parent.getFileName().toString().equals("bin")
&& Files.exists(parent.getParent().resolve("release"))) {
target = parent.getParent();
} else {
target = java.getBinary();
}
FXUtils.showFileInExplorer(target);
}
public void onRemove() {
if (java.isManaged()) {
Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL);
} else {
String path = java.getBinary().toString();
ConfigHolder.globalConfig().getUserJava().remove(path);
ConfigHolder.globalConfig().getDisabledJava().add(path);
try {
JavaManager.removeJava(java);
} catch (InterruptedException ignored) {
}
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaRuntimeItemSkin(this);
}
}
private static final class JavaRuntimeItemSkin extends SkinBase<JavaItem> {
JavaRuntimeItemSkin(JavaItem control) {
super(control);
JavaRuntime java = control.getJava();
String vendor = JavaInfo.normalizeVendor(java.getVendor());
BorderPane root = new BorderPane();
HBox center = new HBox();
center.setMouseTransparent(true);
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion());
item.setSubtitle(java.getBinary().toString());
item.getTags().add(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
if (vendor != null)
item.getTags().add(i18n("java.info.vendor") + ": " + vendor);
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(item);
root.setCenter(center);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
revealButton.setOnAction(e -> control.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("java.reveal"));
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setOnAction(e -> Controllers.confirm(
java.isManaged() ? i18n("java.uninstall.confirm") : i18n("java.disable.confirm"),
i18n("message.warning"),
control::onRemove,
null
));
if (java.isManaged()) {
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
FXUtils.installFastTooltip(removeButton, i18n("java.uninstall"));
if (JavaRuntime.CURRENT_JAVA != null && java.getBinary().equals(JavaRuntime.CURRENT_JAVA.getBinary()))
removeButton.setDisable(true);
} else {
removeButton.setGraphic(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
FXUtils.installFastTooltip(removeButton, i18n("java.disable"));
}
right.getChildren().setAll(revealButton, removeButton);
}
root.setRight(right);
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
getChildren().setAll(new RipplerContainer(root));
}
}
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaManagementPage> {
JavaPageSkin(JavaManagementPage skinnable) {
super(skinnable);
}
@Override
protected List<Node> initializeToolbar(JavaManagementPage skinnable) {
ArrayList<Node> res = new ArrayList<>(4);
res.add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, JavaManager::refresh));
if (skinnable.onInstallJava != null) {
res.add(createToolbarButton2(i18n("java.download"), SVG.DOWNLOAD_OUTLINE, skinnable.onInstallJava));
}
res.add(createToolbarButton2(i18n("java.add"), SVG.PLUS, skinnable::onAddJava));
JFXButton disableJava = createToolbarButton2(i18n("java.disabled.management"), SVG.VIEW_LIST, skinnable::onShowRestoreJavaPage);
disableJava.disableProperty().bind(Bindings.isEmpty(ConfigHolder.globalConfig().getDisabledJava()));
res.add(disableJava);
return res;
}
}
}
@@ -0,0 +1,206 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaRestorePage extends ListPageBase<JavaRestorePage.DisabledJavaItem> implements DecoratorPage {
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.fromTitle(i18n("java.disabled.management")));
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener listener;
public JavaRestorePage(ObservableSet<String> disabledJava) {
this.listener = o -> {
ArrayList<DisabledJavaItem> result = new ArrayList<>(disabledJava.size());
for (String path : disabledJava) {
Path realPath = null;
try {
realPath = Paths.get(path).toRealPath();
} catch (IOException ignored) {
}
result.add(new DisabledJavaItem(disabledJava, path, realPath));
}
result.sort((a, b) -> {
if (a.realPath == null && b.realPath != null)
return -1;
if (a.realPath != null && b.realPath == null)
return 1;
return a.path.compareTo(b.path);
});
this.setItems(FXCollections.observableList(result));
};
disabledJava.addListener(new WeakInvalidationListener(listener));
listener.invalidated(disabledJava);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaRestorePageSkin(this);
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
static final class DisabledJavaItem extends Control {
final ObservableSet<String> disabledJava;
final String path;
final Path realPath;
DisabledJavaItem(ObservableSet<String> disabledJava, String path, Path realPath) {
this.disabledJava = disabledJava;
this.path = path;
this.realPath = realPath;
}
@Override
protected Skin<?> createDefaultSkin() {
return new DisabledJavaItemSkin(this);
}
void onReveal() {
if (realPath != null) {
Path target;
Path parent = realPath.getParent();
if (parent != null
&& parent.getParent() != null
&& parent.getFileName() != null
&& parent.getFileName().toString().equals("bin")
&& Files.exists(parent.getParent().resolve("release"))) {
target = parent.getParent();
} else {
target = realPath;
}
FXUtils.showFileInExplorer(target);
}
}
void onRestore() {
disabledJava.remove(path);
JavaManager.getAddJavaTask(realPath).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
void onRemove() {
disabledJava.remove(path);
}
}
private static final class DisabledJavaItemSkin extends SkinBase<DisabledJavaItem> {
DisabledJavaItemSkin(DisabledJavaItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
Label label = new Label(skinnable.path);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
root.setCenter(label);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
revealButton.setOnAction(e -> skinnable.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("java.reveal"));
if (skinnable.realPath == null) {
revealButton.setDisable(true);
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
removeButton.setOnAction(e -> skinnable.onRemove());
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
right.getChildren().setAll(revealButton, removeButton);
} else {
JFXButton restoreButton = new JFXButton();
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
restoreButton.setOnAction(e -> skinnable.onRestore());
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
right.getChildren().setAll(revealButton, restoreButton);
}
}
root.setRight(right);
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
this.getChildren().setAll(new RipplerContainer(root));
}
}
private static final class JavaRestorePageSkin extends ToolbarListPageSkin<JavaRestorePage> {
JavaRestorePageSkin(JavaRestorePage skinnable) {
super(skinnable);
}
@Override
protected List<Node> initializeToolbar(JavaRestorePage skinnable) {
return Collections.emptyList();
}
}
}
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.PageAware;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -40,6 +41,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings")));
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
private final TabControl.Tab<JavaManagementPage> javaManagementTab = new TabControl.Tab<>("javaManagementPage");
private final TabHeader.Tab<SettingsPage> settingsTab = new TabHeader.Tab<>("settingsPage");
private final TabHeader.Tab<PersonalizationPage> personalizationTab = new TabHeader.Tab<>("personalizationPage");
private final TabHeader.Tab<DownloadSettingsPage> downloadTab = new TabHeader.Tab<>("downloadSettingsPage");
@@ -50,13 +52,14 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
public LauncherSettingsPage() {
gameTab.setNodeSupplier(() -> new VersionSettingsPage(true));
javaManagementTab.setNodeSupplier(JavaManagementPage::new);
settingsTab.setNodeSupplier(SettingsPage::new);
personalizationTab.setNodeSupplier(PersonalizationPage::new);
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
helpTab.setNodeSupplier(HelpPage::new);
feedbackTab.setNodeSupplier(FeedbackPage::new);
aboutTab.setNodeSupplier(AboutPage::new);
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab = new TabHeader(gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab.select(gameTab);
gameTab.initializeIfNeeded();
@@ -74,6 +77,12 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
runInFX(() -> FXUtils.installFastTooltip(settingsItem, i18n("settings.type.global.manage")));
settingsItem.setOnAction(e -> tab.select(gameTab));
})
.addNavigationDrawerItem(javaItem -> {
javaItem.setTitle(i18n("java.management"));
javaItem.setLeftGraphic(wrap(SVG.WRENCH_OUTLINE));
javaItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(javaManagementTab));
javaItem.setOnAction(e -> tab.select(javaManagementTab));
})
.startCategory(i18n("launcher"))
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.launcher.general"));
@@ -237,10 +237,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
doAnimation(show);
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO, () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
});
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
.addAction(i18n("button.view"), () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
})
.addCancel(null)
.build());
}
}
@@ -30,6 +30,7 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -191,10 +192,19 @@ public abstract class SettingsView extends StackPane {
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnMouseClicked(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnMouseClicked(e -> onExportLogs());
logButton.getStyleClass().add("jfx-button-border");
debugPane.setRight(logButton);
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
settingsPane.getContent().add(debugPane);
}
@@ -205,6 +215,10 @@ public abstract class SettingsView extends StackPane {
}
}
public void openLogFolder() {
FXUtils.openFolder(Metadata.HMCL_DIRECTORY.resolve("logs").toFile());
}
protected abstract void onUpdate();
protected abstract void onExportLogs();
@@ -381,8 +381,6 @@ public class DownloadPage extends Control implements DecoratorPage {
{
StackPane graphicPane = new StackPane();
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
content.setTitle(dataItem.getName());
@@ -390,11 +388,16 @@ public class DownloadPage extends Control implements DecoratorPage {
switch (dataItem.getVersionType()) {
case Alpha:
content.getTags().add(i18n("version.game.snapshot"));
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Beta:
content.getTags().add(i18n("version.game.snapshot"));
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Release:
content.getTags().add(i18n("version.game.release"));
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
}
@@ -457,7 +460,7 @@ public class DownloadPage extends Control implements DecoratorPage {
this.setBody(box);
JFXButton downloadButton = new JFXButton(i18n("download"));
JFXButton downloadButton = new JFXButton(i18n("mods.install"));
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
@@ -466,7 +469,7 @@ public class DownloadPage extends Control implements DecoratorPage {
selfPage.download(version);
});
JFXButton saveAsButton = new JFXButton(i18n("button.save_as"));
JFXButton saveAsButton = new JFXButton(i18n("mods.save_as"));
saveAsButton.getStyleClass().add("dialog-accept");
saveAsButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
@@ -79,6 +79,12 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
// Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine
for (InstallerItem item : group.getLibraries()) {
String libraryId = item.getLibraryId();
// Skip fabric-api and quilt-api
if (libraryId.contains("fabric-api") || libraryId.contains("quilt-api")) {
continue;
}
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
if (libraryVersion != null) {
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
@@ -33,6 +34,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod;
@@ -59,15 +61,11 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -117,7 +115,12 @@ class ModListPageSkin extends SkinBase<ModListPage> {
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
searchField.setOnAction(e -> search());
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> search());
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
@@ -297,25 +300,65 @@ class ModListPageSkin extends SkinBase<ModListPage> {
titleContainer.setSpacing(8);
ImageView imageView = new ImageView();
if (StringUtils.isNotBlank(modInfo.getModInfo().getLogoPath())) {
Task.supplyAsync(() -> {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
Path iconPath = fs.getPath(modInfo.getModInfo().getLogoPath());
Task.supplyAsync(() -> {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
String logoPath = modInfo.getModInfo().getLogoPath();
if (StringUtils.isNotBlank(logoPath)) {
Path iconPath = fs.getPath(logoPath);
if (Files.exists(iconPath)) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Files.copy(iconPath, stream);
return new ByteArrayInputStream(stream.toByteArray());
try (InputStream stream = Files.newInputStream(iconPath)) {
Image image = new Image(stream, 40, 40, true, true);
if (!image.isError() && image.getWidth() == image.getHeight())
return image;
} catch (Throwable e) {
LOG.warning("Failed to load image " + logoPath, e);
}
}
}
return null;
}).whenComplete(Schedulers.javafx(), (stream, exception) -> {
if (stream != null) {
imageView.setImage(new Image(stream, 40, 40, true, true));
} else {
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
List<String> defaultPaths = new ArrayList<>(Arrays.asList(
"icon.png",
"logo.png",
"mod_logo.png",
"pack.png",
"logoFile.png"
));
String id = modInfo.getModInfo().getId();
if (StringUtils.isNotBlank(id)) {
defaultPaths.addAll(Arrays.asList(
"assets/" + id + "/icon.png",
"assets/" + id.replace("-", "") + "/icon.png",
id + ".png",
id + "-logo.png",
id + "-icon.png",
id + "_logo.png",
id + "_icon.png"
));
}
}).start();
}
for (String path : defaultPaths) {
Path iconPath = fs.getPath(path);
if (Files.exists(iconPath)) {
try (InputStream stream = Files.newInputStream(iconPath)) {
Image image = new Image(stream, 40, 40, true, true);
if (!image.isError() && image.getWidth() == image.getHeight())
return image;
}
}
}
} catch (Exception e) {
LOG.warning("Failed to load icon", e);
}
return null;
}).whenComplete(Schedulers.javafx(), (image, exception) -> {
if (image != null) {
imageView.setImage(image);
} else {
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
}
}).start();
TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName());
@@ -56,8 +56,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> versionSettingsTab = new TabHeader.Tab<>("versionSettingsTab");
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<InstallerListPage> installerListTab = new TabHeader.Tab<>("installerListTab");
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
@@ -68,11 +68,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
{
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
tab = new TabHeader(versionSettingsTab, modListTab, installerListTab, worldListTab);
tab = new TabHeader(versionSettingsTab, installerListTab, modListTab, worldListTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
@@ -128,10 +128,10 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
if (versionSettingsTab.isInitialized())
versionSettingsTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
if (installerListTab.isInitialized())
installerListTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
@@ -247,15 +247,6 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
runInFX(() -> FXUtils.installFastTooltip(versionSettingsItem, i18n("settings.game")));
versionSettingsItem.setOnAction(e -> control.tab.select(control.versionSettingsTab));
AdvancedListItem modListItem = new AdvancedListItem();
modListItem.getStyleClass().add("navigation-drawer-item");
modListItem.setTitle(i18n("mods.manage"));
modListItem.setLeftGraphic(wrap(SVG.PUZZLE));
modListItem.setActionButtonVisible(false);
modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab));
runInFX(() -> FXUtils.installFastTooltip(modListItem, i18n("mods.manage")));
modListItem.setOnAction(e -> control.tab.select(control.modListTab));
AdvancedListItem installerListItem = new AdvancedListItem();
installerListItem.getStyleClass().add("navigation-drawer-item");
installerListItem.setTitle(i18n("settings.tabs.installers"));
@@ -265,6 +256,15 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
runInFX(() -> FXUtils.installFastTooltip(installerListItem, i18n("settings.tabs.installers")));
installerListItem.setOnAction(e -> control.tab.select(control.installerListTab));
AdvancedListItem modListItem = new AdvancedListItem();
modListItem.getStyleClass().add("navigation-drawer-item");
modListItem.setTitle(i18n("mods.manage"));
modListItem.setLeftGraphic(wrap(SVG.PUZZLE));
modListItem.setActionButtonVisible(false);
modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab));
runInFX(() -> FXUtils.installFastTooltip(modListItem, i18n("mods.manage")));
modListItem.setOnAction(e -> control.tab.select(control.modListTab));
AdvancedListItem worldListItem = new AdvancedListItem();
worldListItem.getStyleClass().add("navigation-drawer-item");
worldListItem.setTitle(i18n("world.manage"));
@@ -276,8 +276,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
AdvancedListBox sideBar = new AdvancedListBox()
.add(versionSettingsItem)
.add(modListItem)
.add(installerListItem)
.add(modListItem)
.add(worldListItem);
VBox.setVgrow(sideBar, Priority.ALWAYS);
@@ -22,20 +22,22 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -46,18 +48,13 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -72,7 +69,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private Profile profile;
private WeakListenerHolder listenerHolder;
private String versionId;
private boolean javaItemsLoaded;
private final VBox rootPane;
private final JFXTextField txtWidth;
@@ -83,9 +79,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final JFXCheckBox chkAutoAllocate;
private final JFXCheckBox chkFullscreen;
private final ComponentSublist javaSublist;
private final MultiFileItem<Pair<JavaVersionType, JavaVersion>> javaItem;
private final MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>> javaAutoDeterminedOption;
private final MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>> javaCustomOption;
private final MultiFileItem<Pair<JavaVersionType, JavaRuntime>> javaItem;
private final MultiFileItem.Option<Pair<JavaVersionType, JavaRuntime>> javaAutoDeterminedOption;
private final MultiFileItem.StringOption<Pair<JavaVersionType, JavaRuntime>> javaVersionOption;
private final MultiFileItem.FileOption<Pair<JavaVersionType, JavaRuntime>> javaCustomOption;
private final ComponentSublist gameDirSublist;
private final MultiFileItem<GameDirectoryType> gameDirItem;
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
@@ -93,9 +91,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final OptionToggleButton showLogsPane;
private final ImagePickerItem iconPickerItem;
private final ChangeListener<Collection<JavaRuntime>> javaListChangeListener;
private final InvalidationListener specificSettingsListener;
private final InvalidationListener javaListener = any -> initJavaSubtitle();
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
private final StringProperty selectedVersion = new SimpleStringProperty();
private final BooleanProperty navigateToSpecificSettings = new SimpleBooleanProperty(false);
@@ -179,8 +179,36 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
javaSublist.setTitle(i18n("settings.game.java_directory"));
javaSublist.setHasSubtitle(true);
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), pair(JavaVersionType.AUTO, null));
javaCustomOption = new MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null))
javaVersionOption = new MultiFileItem.StringOption<>(i18n("settings.game.java_directory.version"), pair(JavaVersionType.VERSION, null));
javaVersionOption.setValidators(new NumberValidator(true));
FXUtils.setLimitWidth(javaVersionOption.getCustomField(), 40);
javaCustomOption = new MultiFileItem.FileOption<Pair<JavaVersionType, JavaRuntime>>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null))
.setChooserTitle(i18n("settings.game.java_directory.choose"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
javaListChangeListener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), allJava -> {
List<MultiFileItem.Option<Pair<JavaVersionType, JavaRuntime>>> options = new ArrayList<>();
options.add(javaAutoDeterminedOption);
options.add(javaVersionOption);
if (allJava != null) {
boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && allJava.stream().allMatch(java -> java.getArchitecture().isX86());
for (JavaRuntime java : allJava) {
options.add(new MultiFileItem.Option<>(
i18n("settings.game.java_directory.template",
java.getVersion(),
isX86 ? i18n("settings.game.java_directory.bit", java.getBits().getBit())
: java.getPlatform().getArchitecture().getDisplayName()),
pair(JavaVersionType.DETECTED, java))
.setSubtitle(java.getBinary().toString()));
}
}
options.add(javaCustomOption);
javaItem.loadChildren(options);
initializeSelectedJava();
});
gameDirItem = new MultiFileItem<>();
gameDirSublist = new ComponentSublist();
@@ -429,31 +457,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
private void initialize() {
memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID));
Task.supplyAsync(JavaVersion::getJavas).thenAcceptAsync(Schedulers.javafx(), list -> {
boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && list.stream().allMatch(java -> java.getArchitecture().isX86());
List<MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>>> options = list.stream()
.map(javaVersion -> new MultiFileItem.Option<>(
i18n("settings.game.java_directory.template",
javaVersion.getVersion(),
isX86 ? i18n("settings.game.java_directory.bit", javaVersion.getBits().getBit())
: javaVersion.getPlatform().getArchitecture().getDisplayName()),
pair(JavaVersionType.DETECTED, javaVersion))
.setSubtitle(javaVersion.getBinary().toString()))
.collect(Collectors.toList());
options.add(0, javaAutoDeterminedOption);
options.add(javaCustomOption);
javaItem.loadChildren(options);
javaItemsLoaded = true;
initializeSelectedJava();
}).start();
javaItem.setSelectedData(null);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus());
enableSpecificSettings.addListener((a, b, newValue) -> {
if (versionId == null) return;
@@ -472,7 +476,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
@Override
@SuppressWarnings("unchecked")
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
@@ -517,8 +520,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.unbindEnum(cboProcessPriority);
lastVersionSetting.usesGlobalProperty().removeListener(specificSettingsListener);
lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener);
lastVersionSetting.javaDirProperty().removeListener(javaListener);
lastVersionSetting.javaProperty().removeListener(javaListener);
lastVersionSetting.defaultJavaPathPropertyProperty().removeListener(javaListener);
lastVersionSetting.javaVersionProperty().removeListener(javaListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().unbind();
@@ -531,6 +536,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
// unbind data fields
javaItem.setToggleSelectedListener(null);
javaVersionOption.valueProperty().unbind();
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
@@ -551,18 +557,42 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.getSelectedData() == null || updatingSelectedJava)
return;
updatingJavaSetting = true;
if (javaVersionOption.isSelected()) {
javaVersionOption.valueProperty().bindBidirectional(versionSetting.javaVersionProperty());
} else {
javaVersionOption.valueProperty().unbind();
javaVersionOption.setValue("");
}
if (javaCustomOption.isSelected()) {
versionSetting.setUsesCustomJavaDir();
} else if (javaAutoDeterminedOption.isSelected()) {
versionSetting.setJavaAutoSelected();
} else if (javaVersionOption.isSelected()) {
if (versionSetting.getJavaVersionType() != JavaVersionType.VERSION)
versionSetting.setJavaVersion("");
versionSetting.setJavaVersionType(JavaVersionType.VERSION);
versionSetting.setDefaultJavaPath(null);
} else {
versionSetting.setJavaVersion(((Pair<JavaVersionType, JavaVersion>) newValue.getUserData()).getValue());
@SuppressWarnings("unchecked")
JavaRuntime java = ((Pair<JavaVersionType, JavaRuntime>) newValue.getUserData()).getValue();
versionSetting.setJavaVersionType(JavaVersionType.DETECTED);
versionSetting.setJavaVersion(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
}
updatingJavaSetting = false;
});
versionSetting.javaVersionTypeProperty().addListener(javaListener);
versionSetting.javaDirProperty().addListener(javaListener);
versionSetting.defaultJavaPathPropertyProperty().addListener(javaListener);
versionSetting.javaProperty().addListener(javaListener);
versionSetting.javaVersionProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
@@ -576,52 +606,101 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !javaItemsLoaded /* JREs are still being loaded */) {
if (lastVersionSetting == null || updatingJavaSetting)
return;
}
if (lastVersionSetting.isUsesCustomJavaDir()) {
javaCustomOption.setSelected(true);
} else if (lastVersionSetting.isJavaAutoSelected()) {
javaAutoDeterminedOption.setSelected(true);
} else {
// javaLoading.set(true);
lastVersionSetting.getJavaVersion(null, null)
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> {
javaItem.setSelectedData(pair(JavaVersionType.DETECTED, javaVersion));
// javaLoading.set(false);
}).start();
updatingSelectedJava = true;
switch (lastVersionSetting.getJavaVersionType()) {
case CUSTOM:
javaCustomOption.setSelected(true);
break;
case VERSION:
javaVersionOption.setSelected(true);
javaVersionOption.setValue(lastVersionSetting.getJavaVersion());
break;
case AUTO:
javaAutoDeterminedOption.setSelected(true);
break;
default:
Toggle toggle = null;
if (JavaManager.isInitialized()) {
try {
JavaRuntime java = lastVersionSetting.getJava(null, null);
if (java != null) {
for (Toggle t : javaItem.getGroup().getToggles()) {
if (t.getUserData() != null) {
@SuppressWarnings("unchecked")
Pair<JavaVersionType, JavaRuntime> userData = (Pair<JavaVersionType, JavaRuntime>) t.getUserData();
if (userData.getValue() != null && java.getBinary().equals(userData.getValue().getBinary())) {
toggle = t;
break;
}
}
}
}
} catch (InterruptedException ignored) {
}
}
if (toggle != null) {
toggle.setSelected(true);
} else {
Toggle selectedToggle = javaItem.getGroup().getSelectedToggle();
if (selectedToggle != null) {
selectedToggle.setSelected(false);
}
}
break;
}
updatingSelectedJava = false;
}
private void initJavaSubtitle() {
FXUtils.checkFxUserThread();
initializeSelectedJava();
VersionSetting versionSetting = lastVersionSetting;
if (versionSetting == null)
if (lastVersionSetting == null)
return;
Profile profile = this.profile;
initializeSelectedJava();
HMCLGameRepository repository = this.profile.getRepository();
String versionId = this.versionId;
boolean autoSelected = versionSetting.isJavaAutoSelected();
JavaVersionType javaVersionType = lastVersionSetting.getJavaVersionType();
boolean autoSelected = javaVersionType == JavaVersionType.AUTO || javaVersionType == JavaVersionType.VERSION;
if (autoSelected && versionId == null) {
if (versionId == null && autoSelected) {
javaSublist.setSubtitle(i18n("settings.game.java_directory.auto"));
return;
}
Task.composeAsync(Schedulers.javafx(), () -> {
Pair<JavaVersionType, JavaRuntime> selectedData = javaItem.getSelectedData();
if (selectedData != null && selectedData.getValue() != null) {
javaSublist.setSubtitle(selectedData.getValue().getBinary().toString());
return;
}
if (JavaManager.isInitialized()) {
GameVersionNumber gameVersionNumber;
Version version;
if (versionId == null) {
return versionSetting.getJavaVersion(GameVersionNumber.unknown(), null);
gameVersionNumber = GameVersionNumber.unknown();
version = null;
} else {
return versionSetting.getJavaVersion(
GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId)),
profile.getRepository().getVersion(versionId));
gameVersionNumber = GameVersionNumber.asGameVersion(repository.getGameVersion(versionId));
version = repository.getVersion(versionId);
}
}).thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
.map(JavaVersion::getBinary).map(Path::toString).orElseGet(() ->
autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid"))))
.start();
try {
JavaRuntime java = lastVersionSetting.getJava(gameVersionNumber, version);
if (java != null) {
javaSublist.setSubtitle(java.getBinary().toString());
} else {
javaSublist.setSubtitle(autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid"));
}
return;
} catch (InterruptedException ignored) {
}
}
javaSublist.setSubtitle("");
}
private void editSpecificSettings() {
@@ -664,10 +743,4 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private enum JavaVersionType {
DETECTED,
CUSTOM,
AUTO,
}
}
@@ -23,7 +23,8 @@ public interface WizardPage {
default void onNavigate(Map<String, Object> settings) {
}
void cleanup(Map<String, Object> settings);
default void cleanup(Map<String, Object> settings) {
}
String getTitle();
}
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
@@ -177,7 +177,7 @@ public final class UpdateHandler {
private static void startJava(Path jar, String... appArgs) throws IOException {
List<String> commandline = new ArrayList<>();
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
commandline.add(JavaRuntime.getDefault().getBinary().toString());
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
Object key = entry.getKey();
if (key instanceof String && ((String) key).startsWith("hmcl.")) {
@@ -1,57 +0,0 @@
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @see <a href="https://github.com/fractureiser-investigation/fractureiser">fractureiser-investigation/fractureiser</a>
* @see <a href="https://prismlauncher.org/news/cf-compromised-alert/#automated-script">[MALWARE WARNING] "fractureiser" malware in many popular Minecraft mods and modpacks</a>
*/
public final class FractureiserDetector {
private FractureiserDetector() {
}
private static final class FractureiserException extends Exception {
}
public static boolean detect() {
try {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Path appdata = Paths.get(System.getProperty("user.home"), "AppData");
if (Files.isDirectory(appdata)) {
check(appdata.resolve("Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\run.bat"));
Path falseEdgePath = appdata.resolve("Local\\Microsoft Edge");
if (Files.exists(falseEdgePath)) {
check(falseEdgePath.resolve(".ref"));
check(falseEdgePath.resolve("client.jar"));
check(falseEdgePath.resolve("lib.dll"));
check(falseEdgePath.resolve("libWebGL64.jar"));
check(falseEdgePath.resolve("run.bat"));
}
}
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
Path dataDir = Paths.get(System.getProperty("user.home"), ".config", ".data");
if (Files.exists(dataDir)) {
check(dataDir.resolve(".ref"));
check(dataDir.resolve("client.jar"));
check(dataDir.resolve("lib.jar"));
}
}
} catch (FractureiserException e) {
return true;
} catch (Throwable ignored) {
}
return false;
}
private static void check(Path path) throws FractureiserException {
if (Files.isRegularFile(path)) {
throw new FractureiserException();
}
}
}
@@ -17,12 +17,11 @@
*/
package org.jackhuang.hmcl.util;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -34,6 +33,7 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
@@ -51,9 +51,7 @@ public final class NativePatcher {
return natives.computeIfAbsent(platform, p -> {
//noinspection ConstantConditions
try (Reader reader = new InputStreamReader(NativePatcher.class.getResourceAsStream("/assets/natives.json"), StandardCharsets.UTF_8)) {
Map<String, Map<String, Library>> natives = JsonUtils.GSON.fromJson(reader, new TypeToken<Map<String, Map<String, Library>>>() {
}.getType());
Map<String, Map<String, Library>> natives = JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, mapTypeOf(String.class, Library.class)));
return natives.getOrDefault(p.toString(), Collections.emptyMap());
} catch (IOException e) {
LOG.warning("Failed to load native library list", e);
@@ -62,7 +60,7 @@ public final class NativePatcher {
});
}
public static Version patchNative(Version version, String gameVersion, JavaVersion javaVersion, VersionSetting settings) {
public static Version patchNative(Version version, String gameVersion, JavaRuntime javaVersion, VersionSetting settings) {
if (settings.getNativesDirType() == NativesDirectoryType.CUSTOM) {
if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0)
return version;
@@ -154,7 +152,7 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static Library getMesaLoader(JavaVersion javaVersion, Renderer renderer) {
public static Library getMesaLoader(JavaRuntime javaVersion, Renderer renderer) {
return getNatives(javaVersion.getPlatform()).get(renderer == Renderer.LLVMPIPE ? "software-renderer-loader" : "mesa-loader");
}
}
@@ -41,13 +41,13 @@
*/
package org.jackhuang.hmcl.util;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.Platform;
import javax.swing.*;
@@ -67,9 +67,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
// From: https://github.com/Col-E/Recaf/blob/7378b397cee664ae81b7963b0355ef8ff013c3a7/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
public final class SelfDependencyPatcher {
@@ -104,28 +105,14 @@ public final class SelfDependencyPatcher {
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies").resolve(Platform.getPlatform().toString()).resolve("openjfx");
static List<DependencyDescriptor> readDependencies() {
ArrayList<DependencyDescriptor> dependencies;
//noinspection ConstantConditions
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
Map<String, ArrayList<DependencyDescriptor>> allDependencies =
new Gson().fromJson(reader, new TypeToken<Map<String, ArrayList<DependencyDescriptor>>>(){}.getType());
dependencies = allDependencies.get(Platform.getPlatform().toString());
Map<String, List<DependencyDescriptor>> allDependencies =
JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, listTypeOf(DependencyDescriptor.class)));
return allDependencies.get(Platform.getPlatform().toString());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (dependencies == null) return null;
try {
ClassLoader classLoader = SelfDependencyPatcher.class.getClassLoader();
Class.forName("netscape.javascript.JSObject", false, classLoader);
Class.forName("org.w3c.dom.html.HTMLDocument", false, classLoader);
} catch (Throwable e) {
LOG.warning("Disable javafx.web because JRE is incomplete", e);
dependencies.removeIf(it -> "javafx.web".equals(it.module) || "javafx.media".equals(it.module));
}
return dependencies;
}
public String module;
@@ -188,7 +175,7 @@ public final class SelfDependencyPatcher {
// So the problem with Java 8 is that some distributions DO NOT BUNDLE JAVAFX
// Why is this a problem? OpenJFX does not come in public bundles prior to Java 11
// So you're out of luck unless you change your JDK or update Java.
if (CURRENT_JAVA.getParsedVersion() < 11) {
if (JavaRuntime.CURRENT_VERSION < 11) {
throw new IncompatibleVersionException();
}
@@ -20,8 +20,8 @@ package org.jackhuang.hmcl.util.i18n;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.IOException;
import java.util.List;
@@ -116,7 +116,7 @@ public final class Locales {
if (resourceBundle == null) {
if (this != DEFAULT && this.locale == DEFAULT.locale) {
bundle = DEFAULT.getResourceBundle();
} else if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) {
} else if (JavaInfo.CURRENT_ENVIRONMENT.getParsedVersion() < 9) {
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, UTF8Control.INSTANCE);
} else {
// Java 9+ uses UTF-8 as the default encoding for resource bundles
@@ -0,0 +1,62 @@
[
{
"title" : "OpenJFX",
"subtitle" : "Copyright © 2013, 2024, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.",
"externalLink" : "https://openjfx.io"
},
{
"title" : "JFoenix",
"subtitle" : "Copyright © 2016 JFoenix.\nLicensed under the MIT License.",
"externalLink" : "https://github.com/sshahine/JFoenix"
},
{
"title" : "Gson",
"subtitle" : "Copyright © 2008 Google Inc.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/google/gson"
},
{
"title" : "Apache Commons Compress",
"subtitle" : "Licensed under the Apache 2.0 License.",
"externalLink" : "https://commons.apache.org/proper/commons-compress/"
},
{
"title" : "XZ for Java",
"subtitle" : "Lasse Collin, Igor Pavlov, and/or Brett Okken.\nPublic Domain.",
"externalLink" : "https://tukaani.org/xz/java.html"
},
{
"title" : "FX Gson",
"subtitle" : "Copyright © 2016 Joffrey Bion.\nLicensed under the MIT License.",
"externalLink" : "https://github.com/joffrey-bion/fx-gson"
},
{
"title" : "jsoup",
"subtitle" : "Copyright © 2009 - 2024 Jonathan Hedley (https://jsoup.org/)\nLicensed under the MIT License.",
"externalLink" : "https://jsoup.org/"
},
{
"title" : "NanoHTTPD",
"subtitle" : "Copyright © 2012 - 2015 nanohttpd.\nLicensed under the BSD 3-clause License.",
"externalLink" : "https://github.com/NanoHttpd/nanohttpd"
},
{
"title" : "Constant Pool Scanner",
"subtitle" : "Copyright © 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.",
"externalLink" : "https://github.com/jenkinsci/constant-pool-scanner"
},
{
"title" : "OpenNBT",
"subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.",
"externalLink" : "https://github.com/GeyserMC/OpenNBT"
},
{
"title" : "minecraft-jfx-skin",
"subtitle" : "Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.",
"externalLink" : "https://github.com/InfinityStudio/minecraft-jfx-skin"
},
{
"title" : "SimplePNG",
"subtitle" : "Copyright © 2023 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/Glavo/SimplePNG"
}
]
@@ -0,0 +1,78 @@
[
{
"image" : "/assets/img/yushijinhun.png",
"title" : "yushijinhun",
"subtitleLocalized" : "about.thanks_to.yushijinhun.statement",
"externalLink" : "https://github.com/yushijinhun"
},
{
"image" : "/assets/img/bangbang93.png",
"title" : "bangbang93",
"subtitleLocalized" : "about.thanks_to.bangbang93.statement",
"externalLink" : "https://www.bangbang93.com"
},
{
"image" : "/assets/img/glavo.png",
"title" : "Glavo",
"subtitleLocalized" : "about.thanks_to.glavo.statement",
"externalLink" : "https://github.com/Glavo"
},
{
"image" : "/assets/img/zekerzhayard.png",
"title" : "ZekerZhayard",
"subtitleLocalized" : "about.thanks_to.zekerzhayard.statement",
"externalLink" : "https://github.com/ZekerZhayard"
},
{
"image" : "/assets/img/zkitefly.png",
"title" : "Zkitefly",
"subtitleLocalized" : "about.thanks_to.zkitefly.statement",
"externalLink" : "https://github.com/zkitefly"
},
{
"image" : "/assets/img/burningtnt.png",
"title" : "Burning_TNT",
"subtitleLocalized" : "about.thanks_to.burningtnt.statement",
"externalLink" : "https://github.com/burningtnt"
},
{
"image" : "/assets/img/ShulkerSakura.png",
"title" : "ShulkerSakura",
"subtitleLocalized" : "about.thanks_to.shulkersakura.statement",
"externalLink" : "https://github.com/ShulkerSakura"
},
{
"image" : "/assets/img/gamerteam.png",
"title" : "gamerteam",
"subtitleLocalized" : "about.thanks_to.gamerteam.statement",
"externalLink" : "https://github.com/ZhaiSoul"
},
{
"image" : "/assets/img/red_lnn.png",
"title" : "Red_lnn",
"subtitleLocalized" : "about.thanks_to.red_lnn.statement"
},
{
"image" : "/assets/img/mcmod.png",
"titleLocalized" : "about.thanks_to.mcmod",
"subtitleLocalized" : "about.thanks_to.mcmod.statement",
"externalLink" : "https://www.mcmod.cn"
},
{
"image" : "/assets/img/chest.png",
"titleLocalized" : "about.thanks_to.mcbbs",
"subtitleLocalized" : "about.thanks_to.mcbbs.statement"
},
{
"image" : "/assets/img/github.png",
"titleLocalized" : "about.thanks_to.contributors",
"subtitleLocalized" : "about.thanks_to.contributors.statement",
"externalLink" : "https://github.com/HMCL-dev/HMCL/graphs/contributors"
},
{
"image" : "/assets/img/icon.png",
"titleLocalized" : "about.thanks_to.users",
"subtitleLocalized" : "about.thanks_to.users.statement",
"externalLink" : "https://docs.hmcl.net/groups.html"
}
]
+34 -4
View File
@@ -431,7 +431,6 @@
.jfx-layout-heading {
-fx-font-size: 20.0px;
-fx-font-family: -fx-base-font-family;
-fx-alignment: center-left;
-fx-padding: 5.0 0.0 5.0 0.0;
}
@@ -449,18 +448,15 @@
.dialog-error {
-fx-text-fill: #d32f2f;
-fx-font-family: -fx-base-font-family;
-fx-padding: 0.7em 0.8em;
}
.dialog-accept {
-fx-text-fill: #03A9F4;
-fx-font-family: -fx-base-font-family;
-fx-padding: 0.7em 0.8em;
}
.dialog-cancel {
-fx-font-family: -fx-base-font-family;
-fx-padding: 0.7em 0.8em;
}
@@ -1438,3 +1434,37 @@
.fit-width {
-fx-pref-width: 100%;
}
/*******************************************************************************
* *
* HTML Renderer *
* *
*******************************************************************************/
.html {
-fx-font-size: 16;
}
.html-hyperlink {
-fx-fill: blue;
}
.html-h1 {
-fx-font-size: 22;
}
.html-h2 {
-fx-font-size: 20;
}
.html-h3 {
-fx-font-size: 18;
}
.html-bold {
-fx-font-weight: bold;
}
.html-italic {
-fx-font-style: italic;
}
@@ -183,6 +183,7 @@ button.retry=Retry
button.save=Save
button.save_as=Save As
button.select_all=Select All
button.view=View
button.yes=Yes
chat=Join Group Chat
@@ -211,6 +212,7 @@ curse.category.4482=Extra Large
curse.category.4472=Tech
curse.category.4473=Magic
curse.category.5128=Vanilla+
curse.category.7418=Horror
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5299=Education
@@ -333,6 +335,7 @@ download.provider.official=From Official Sources
download.provider.balanced=From Fastest Available
download.provider.mirror=From Mirror
download.java=Downloading Java
download.java.override=This Java version already exists, do you want to uninstall and reinstall it?
download.javafx=Downloading dependencies for the launcher...
download.javafx.notes=We are currently downloading dependencies for HMCL from the Internet.\n\
\n\
@@ -351,8 +354,6 @@ extension.mod=Mod File
extension.png=Image File
extension.ps1=Windows PowerShell Script(.ps1)
extension.sh=Shell Script(.sh)
fatal.fractureiser=Hello Minecraft! Launcher has detected that your computer has been infected with the Fraureiser virus, which poses a serious security issue.\n\
Please use antivirus software to perform a full scan immediately, and then change the passwords of all accounts you have logged in on this computer.
fatal.javafx.incompatible=Missing JavaFX environment.\n\
HMCL cannot automatically install JavaFX under Java versions below 11.\n\
Please update your Java to version 11 or higher.
@@ -645,6 +646,36 @@ install.new_game.malformed=Invalid Name
install.select=Select an operation
install.success=Installed successfully.
java.add=Add Java
java.add.failed=This Java is invalid or incompatible with the current platform.
java.disable=Disable Java
java.disable.confirm=Are you sure you want to disable this Java?
java.disabled.management=Disabled Java
java.disabled.management.remove=Remove this Java from the list
java.disabled.management.restore=Re-enable this Java
java.download=Download Java
java.download.load_list.failed=Failed to load version list
java.download.more=More Java distributions
java.download.prompt=Please choose the Java version you want to download:
java.download.distribution=Distribution
java.download.version=Version
java.download.packageType=Package Type
java.management=Java Management
java.info.architecture=Architecture
java.info.vendor=Vendor
java.info.version=Version
java.info.disco.distribution=Distribution
java.install=Install Java
java.install.archive=Source Path
java.install.failed.exists=This name is already owned
java.install.failed.invalid=This archive is not a valid Java installation package, so it cannot be installed.
java.install.failed.unsupported_platform=This Java is not compatible with the current platform, so it cannot be installed.
java.install.name=Name
java.install.warning.invalid_character=Illegal character in name
java.reveal=Reveal the Java directory
java.uninstall=Uninstall Java
java.uninstall.confirm=Are you sure you want to uninstall this Java? This action cannot be undone!
lang=English (US)
lang.default=Use System Locales
launch.advice=%s Do you still want to continue to launch?
@@ -663,15 +694,16 @@ launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or l
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions earlier than 1.8.0_51. Please install the latest version of Java 8.
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or newer, please use Java 8 instead.
launch.advice.modded_java=Some Mods may not be compatible with higher versions of Java. It is recommended to use Java %s to start Minecraft %s.
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge, or launch the game with Java 8u312/11.0.13/17.0.1 or earlier.
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version, please try updating Forge.
launch.advice.newer_java=You are using the old Java to start the game. It is recommended to update to Java 8, otherwise some mods may cause the game to crash.
launch.advice.not_enough_space=You have allocated a memory size larger than the actual %d MB of memory installed on your computer. You may experience degraded performance, or even be unable to launch the game.
launch.advice.require_newer_java_version=Minecraft %1$s requires Java %2$s or later, but we could not find one. Do you want to download one now?
launch.advice.require_newer_java_version=Current game version requires Java %s, but we could not find one. Do you want to download one now?
launch.advice.too_large_memory_for_32bit=You have allocated a memory size larger than the memory limitation of the 32-bit Java installation. You may be unable to launch the game.
launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 or below only supports Java 8 for the Linux x86-64 platform, because later versions cannot load 32-bit native libraries like liblwjgl.so\n\
\n\
Please download it from java.com, or install OpenJDK 8.
launch.advice.vanilla_x86.translation=Minecraft is not fully supported for your platform, so you may experience missing functionality, or even be unable to launch the game.\nYou can play through the Rosetta translation environment for a full gaming experience.
launch.advice.unknown=The game cannot be launched due to the following reasons:
launch.failed=Failed to launch
launch.failed.cannot_create_jvm=We are unable to create a Java virtual machine. It may be caused by incorrect Java VM arguments. You can try fixing it by removing all arguments you added under instance settings.
launch.failed.creating_process=We are unable to create a new process, please check your Java path.
@@ -685,6 +717,7 @@ launch.failed.execution_policy.hint=The current execution policy prevents the ex
\n\
Click on 'OK' to allow the current user to execute PowerShell scripts, or click on 'Cancel' to keep it as it is.
launch.failed.exited_abnormally=Game crashed. Please refer to the crash log for more details.
launch.failed.java_version_too_low=The Java version you specified is too low, please reset the Java version.
launch.failed.no_accepted_java=Unable to find a compatible Java version, do you want to start the game with the default Java?\nClick on 'Yes' to start the game with the default Java.\nOr, you can go to the instance settings to select one yourself.
launch.failed.sigkill=Game was forcibly terminated by the user or system.
launch.state.dependencies=Resolving dependencies
@@ -693,7 +726,7 @@ launch.state.java=Checking Java version
launch.state.logging_in=Logging in
launch.state.modpack=Downloading required files
launch.state.waiting_launching=Waiting for the game to launch
launch.wrong_javadir=Invalid Java path, falling back to the default one.
launch.invalid_java=Invalid Java path, please reset the Java path.
launcher=Launcher
launcher.agreement=ToS and EULA
@@ -727,10 +760,8 @@ logwindow.title=Log
logwindow.help=You can go to the HMCL community and find others for help
logwindow.autoscroll=Auto-scroll
logwindow.export_game_crash_logs=Export Crash Logs
logwindow.export_dump.dependency_ok.button=Export Game Stack Dump
logwindow.export_dump.dependency_ok.doing_button=Exporting Game Stack Dump (May take up to 15 seconds)
logwindow.export_dump.no_dependency.button=Export Game Stack Dump (Not compatible)
logwindow.export_dump.no_dependency.tooltip=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL QQ group or HMCL Discord for help.
logwindow.export_dump=Export Game Stack Dump
logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL QQ group or HMCL Discord for help.
main_page=Home
@@ -791,7 +822,6 @@ modpack.origin.mcbbs.prompt=Post ID
modpack.scan=Parsing Modpack Index
modpack.task.install=Import Modpack
modpack.task.install.error=Unable to identify this modpack. We currently only support Curse, Modrinth, MultiMC, and MCBBS modpacks.
modpack.task.install.will=The location of the modpack you are going to install:
modpack.type.curse=Curse
modpack.type.curse.tolerable_error=Unable to download dependencies, you can try continuing to download by launching this game instance.
modpack.type.curse.error=Unable to download dependencies, please try again or use a proxy connection.
@@ -933,6 +963,8 @@ mods.not_modded=You must install a mod loader (Fabric, Forge, Quilt or LiteLoade
mods.restore=Rollback
mods.url=Official Page
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot start. Are you sure you want to update?
mods.install=Install
mods.save_as=Save As
nbt.entries=%s entries
nbt.open.failed=Fail to open file
@@ -945,6 +977,10 @@ datapack.choose_datapack=Select a datapack to import
datapack.extension=Datapack
datapack.title=World %s - Datapacks
web.failed=Page loading failed
web.open_in_browser=Do you want to open this address in a browser:\n%s
web.view_in_browser=View in browser
world=Worlds
world.add=Add a World (.zip)
world.datapack=Manage Datapacks
@@ -1120,6 +1156,7 @@ settings.game.java_directory.auto.not_found=No suitable Java version was install
settings.game.java_directory.bit=%s bit
settings.game.java_directory.choose=Select Java path.
settings.game.java_directory.invalid=Incorrect Java path.
settings.game.java_directory.version=Specify Java Version
settings.game.java_directory.template=%s (%s)
settings.game.management=Manage
settings.game.working_directory=Working Directory
@@ -1146,6 +1183,7 @@ settings.launcher.font=Font
settings.launcher.general=General
settings.launcher.language=Language (applies after restart)
settings.launcher.launcher_log.export=Export launcher logs
settings.launcher.launcher_log.reveal=Reveal Logs in Explorer
settings.launcher.launcher_log.export.failed=Unable to export logs
settings.launcher.launcher_log.export.success=Logs have been exported to %s
settings.launcher.log=Logging
@@ -187,6 +187,7 @@ curse.category.4482=Extra grande
curse.category.4472=Tecnología
curse.category.4473=Magia
curse.category.5128=Vanilla+
curse.category.7418=Horror
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5299=Educación
@@ -610,7 +611,6 @@ launch.advice.java9=No puedes ejecutar Minecraft 1.12 o anterior con Java 9 o m
launch.advice.modlauncher8=La versión de Forge que estás utilizando no es compatible con la versión actual de Java. Por favor, intenta actualizar Forge, o ejecutar el juego con Java 8u312/11.0.13/17.0.1 o anterior.
launch.advice.newer_java=Se recomienda Java 8 para una experiencia de juego más fluida. Y para Minecraft 1.12 o superior, y la mayoría de los mods, es obligatorio.
launch.advice.not_enough_space=Has asignado un tamaño de memoria mayor que los %d MB reales de memoria instalados en tu máquina. Es posible que el rendimiento del juego se vea afectado, o incluso que no puedas iniciar el juego.
launch.advice.require_newer_java_version=Minecraft %1$s requiere Java %2$s o posterior, pero no hemos podido encontrar una versión. ¿Desea descargar una ahora?
launch.advice.too_large_memory_for_32bit=Has asignado un tamaño de memoria mayor que la limitación de memoria de la instalación de Java de 32 bits. Es posible que no puedas iniciar el juego.
launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 o inferior sólo admite Java 8 para la plataforma Linux x86-64, porque las versiones posteriores no pueden cargar las bibliotecas nativas de 32 bits como liblwjgl.so\n\
\n\
@@ -639,7 +639,6 @@ launch.state.java=Comprobando la versión de Java
launch.state.logging_in=Iniciando sesión
launch.state.modpack=Descargando dependencias
launch.state.waiting_launching=Esperando la ejecución del juego
launch.wrong_javadir=Ruta de Java no válida, volviendo a la predeterminada.
launcher=Launcher
launcher.agreement=Términos de servicio y EULA
@@ -672,10 +671,8 @@ logwindow.title=Registro
logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar
logwindow.autoscroll=Desplazamiento automático
logwindow.export_game_crash_logs=Exportar registros de errores
logwindow.export_dump.dependency_ok.button=Exportar volcado de pila de juegos
logwindow.export_dump.dependency_ok.doing_button=Exportación de volcado de pila de juego (puede tardar hasta 15 segundos)
logwindow.export_dump.no_dependency.button=Exportar volcado de pila de juegos (no compatible)
logwindow.export_dump.no_dependency.tooltip=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL QQ o HMCL Discord para obtener ayuda.
logwindow.export_dump=Exportar volcado de pila de juegos
logwindow.export_dump.no_dependency=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL QQ o HMCL Discord para obtener ayuda.
main_page=Inicio
@@ -734,8 +731,7 @@ modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=ID de publicación
modpack.scan=Análisis del índice de modpacks
modpack.task.install=Instalar Modpack
modpack.task.install.error=No se ha podido identificar este modpack. Actualmente sólo soportamos los modpacks Curse, Modrinth, MultiMC y MCBBS.
modpack.task.install.will=Vas a instalar el modpack:
modpack.task.install.error=No se ha podido identificar este modpack. Actualmente sólo soportamos los modpacks Curse, Modrinth, MultiMC y MCBBS.
modpack.type.curse=Curse
modpack.type.curse.tolerable_error=No se han podido descargar las dependencias, puedes intentar continuar la descarga ejecutando esta instancia del juego.
modpack.type.curse.error=No se han podido descargar las dependencias de CurseForge, inténtalo de nuevo o utiliza una conexión proxy.
@@ -173,6 +173,7 @@ curse.category.4479=ハードコア
curse.category.4482=特大
curse.category.4472=Tech
curse.category.4473=魔法
curse.category.7418=ホラー
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5129=Vanilla+
@@ -462,7 +463,6 @@ launch.advice.java8_51_1_13=Minecraft 1.13は、1.8.0_51より前のJava8でク
launch.advice.java9=Java9以降のバージョンのJavaでMinecraft1.12以前を起動することはできません。
ゲームを高速化するには、launch.advice.newer_java=Java8をお勧めします。多くのMinecraft1.12以降、およびほとんどのModには、Java8が必要です。
launch.advice.not_enough_space=割り当てたメモリが多すぎます。物理メモリサイズが%dMBであるため、ゲームがクラッシュする可能性があります。
launch.advice.require_newer_java_version=Minecraft %1$s にはJava %2$s 以降が必要です。今すぐダウンロードしますか?
launch.advice.too_large_memory_for_32bit=32ビットJavaランタイム環境が原因で、割り当てたメモリが多すぎるため、ゲームがクラッシュする可能性があります。32ビットシステムの最大メモリ容量は1024MBです。
launch.advice.vanilla_linux_java_8=Linux x86-64の場合、Minecraft1.12.2以下はJava8でのみ実行できます。\nJava9以降のバージョンでは、liblwjgl.soなどの32ビットネイティブライブラリをロードできません。
launch.advice.vanilla_x86.translation=Minecraftは現在、x86およびx86-64以外のアーキテクチャの公式サポートを提供していません。\nJava for x86-64を使用して、トランスレータを介してminecraftを実行するか、プラットフォームの対応するネイティブライブラリをダウンロードして指定してくださいその配置パス。
@@ -484,7 +484,6 @@ launch.state.java=Javaバージョンの検出
launch.state.logging_in=ログイン
launch.state.modpack=modpackを読み込んでいます
launch.state.waiting_launching=ゲームの起動
launch.wrong_javadir=無効なJavaディレクトリ、デフォルトのJavaパスが適用されます。
launcher=ランチャー
launcher.agreement=EULA
@@ -574,7 +573,6 @@ modpack.origin.mcbbs.prompt=スレッドID
modpack.scan=このmodpackをスキャンしています
modpack.task.install=Modpackのインポート
modpack.task.install.error=このmodpackファイルは認識できません。CurseおよびMultiMCmodpackのみがサポートされています。
modpack.task.install.will=modpackをインストールします:
modpack.type.curse=Curse
modpack.type.curse.completion=Cursemodpackに関連するファイルをインストールします
modpack.type.curse.tolerable_error=このmodpackのすべてのファイルのダウンロードを完了できません。対応するゲームバージョンを開始するときに、ダウンロードを再試行できます。ネットワークの問題により、数回再試行する場合があります。
@@ -184,6 +184,7 @@ curse.category.4482=Extra Large
curse.category.4472=Tech
curse.category.4473=Magic
curse.category.5128=Vanilla+
curse.category.7418=Horror
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5299=Education
@@ -486,7 +487,6 @@ launch.advice.java8_51_1_13=Minecraft 1.13 может аварийно заве
launch.advice.java9=Вы не сможете запустить Minecraft 1.12 и ниже с Java 9 и более новыми версиями Java.
launch.advice.newer_java=Рекомендуется использовать Java 8, чтобы игра работала быстрее. Для многих версий Minecraft 1.12 и большинства модов требуется Java 8.
launch.advice.not_enough_space=Вы выделили слишком много памяти, поскольку размер физической памяти составляет %d МБ, ваша игра может рухнуть.
launch.advice.require_newer_java_version=Minecraft %1$s требует Java %2$s или более новую версию, скачать её сейчас?
launch.advice.too_large_memory_for_32bit=Вы выделили слишком много памяти, из-за 32-разрядной Java Runtime Environment ваша игра может рухнуть. Максимальный объем памяти для 32-разрядных систем составляет 1024 МБ.
launch.advice.vanilla_linux_java_8=На Linux x86-64, Minecraft 1.12.2 и ниже может работать только на Java 8.\nВерсии Java 9 и выше не могут загружать 32-битные нативные библиотеки, такие как liblwjgl.so.
launch.advice.modlauncher8=Используемая вами версия Forge несовместима с текущей версией Java. Попробуйте обновить Forge или начните с Java 8u312/11.0.13/17.0.1 или более ранней версии.
@@ -511,7 +511,6 @@ launch.state.java=Проверка версии Java
launch.state.logging_in=Вход в систему
launch.state.modpack=Скачивание зависимостей
launch.state.waiting_launching=Ожидание запуска игры
launch.wrong_javadir=Неверный путь Java, возврат к пути по умолчанию.
launcher=Лаунчер
launcher.agreement=Пользовательское соглашение
@@ -602,7 +601,6 @@ modpack.origin.mcbbs.prompt=ID записи
modpack.scan=Разбор индекса модпака
modpack.task.install=Установить модпак
modpack.task.install.error=Не удаётся идентифицировать этот модпак. Поддерживаются только модпаки Curse, Modrinth, MultiMC и MCBBS.
modpack.task.install.will=Вы собираетесь установить модпак\:
modpack.type.curse=Curse
modpack.type.curse.completion=Установите файлы, связанные с модпаком Curse
modpack.type.curse.tolerable_error=Мы не можем завершить скачивание всех файлов этого модпака. Вы можете повторить скачивание при запуске соответствующей версии игры. Вы можете повторить попытку несколько раз из-за проблем с сетью.
@@ -192,6 +192,7 @@ button.retry=重試
button.save=儲存
button.save_as=另存為
button.select_all=全選
button.view=查看
button.yes=是
chat=官方群組
@@ -221,6 +222,7 @@ curse.category.4482=大型模組包
curse.category.4472=科技
curse.category.4473=魔法
curse.category.5128=原版增強
curse.category.7418=恐怖
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5299=教育
@@ -342,6 +344,7 @@ download.provider.official=儘量使用官方源(最新,但可能加載慢
download.provider.balanced=選擇加載速度快的下載源(平衡,但可能不是最新)
download.provider.mirror=儘量使用鏡像源(加載快,但可能不是最新)
download.java=下載 Java
download.java.override=此 Java 版本已經存在,是否移除並重新安裝?
download.javafx=正在下載必要的運行時組件
download.javafx.notes=正在通過網絡下載 HMCL 必要的運行時組件。\n點擊“切換下載源”按鈕查看詳情以及選擇下載源,點擊“取消”按鈕停止並退出。\n注意:如果下載速度過慢,請嘗試切换下載源。
download.javafx.component=正在下載模塊 %s
@@ -359,7 +362,6 @@ extension.png=圖片檔案
extension.ps1=PowerShell 指令碼
extension.sh=Bash 指令碼
fatal.fractureiser=Hello Minecraft! Launcher 檢測到你的電腦被 Fractureiser 病毒感染,存在嚴重安全問題。\n請立即使用殺毒軟體進行全盤查殺,隨後修改你在此電腦上登入過的所有帳號的密碼。
fatal.javafx.incompatible=缺少 JavaFX 運行環境。\nHMCL 無法在低於 Java 11 的 Java 環境上自行補全 JavaFX 運行環境,請更新到 Java 11 或更高版本。
fatal.javafx.incomplete=JavaFX 運行環境不完整,請嘗試更換你的 Java 或者重新安裝 OpenJFX。
fatal.javafx.missing=缺少 JavaFX 運行環境,請使用包含 OpenJFX 的 Java 運行環境啟動 Hello Minecraft! Launcher。\n你可以訪問 https://docs.hmcl.net/help.html 頁面尋求幫助。
@@ -423,7 +425,7 @@ game.crash.reason.mod_profile_causes_game_crash=當前遊戲因為 Mod 配置文
game.crash.reason.fabric_reports_an_error_and_gives_a_solution=Forge 可能已經提供了錯誤信息。\n你可以查看日誌,並根據錯誤報告中的日誌信息進行對應處。\n如果沒有看到報錯信息,可以查看錯誤報告了解錯誤具體是如何發生的。
game.crash.reason.java_version_is_too_high=當前遊戲因為使用的 Java 版本過高而崩潰了,無法繼續運行。\n請在 全局遊戲設置 或 遊戲特定設置 的 Java 路徑選項卡中改用較低版本的 Java,然後再啟動遊戲。\n如果沒有,可以從 <a href="https://www.java.com/download/">java.com(Java8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE(Java17)</a> 等平台下載、安裝一個(安裝完後需重啟啟動器)。
game.crash.reason.mod_name=當前遊戲因為 Mod 檔案名稱問題,無法繼續運行。\nMod 檔案名稱應只使用英文全半型的大小寫字母(Aa~Zz)、數位(0~9)、橫線(-)、底線(_)和點(.)。\n請到Mod資料夾中將所有不合規的Mod檔案名稱添加一個上述的合規的字元。
game.crash.reason.incomplete_forge_installation=當前遊戲因為 Forge / NeoForge 安裝不完整,無法繼續運行。\n請在 版本設置 - 自動安裝 中卸載 Forge / NeoForge 並重新安裝。
game.crash.reason.incomplete_forge_installation=當前遊戲因為 Forge / NeoForge 安裝不完整,無法繼續運行。\n請在 版本設置 - 自動安裝 中移除 Forge / NeoForge 並重新安裝。
game.crash.reason.file_already_exists=當前遊戲因為文件 %1$s 已經存在,無法繼續運行。\n如果你認為這個文件可以刪除,你可以在備份這個文件後嘗試刪除它,並重新啟動遊戲。
game.crash.reason.file_changed=當前遊戲因為檔案校驗失敗,無法繼續運行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
game.crash.reason.gl_operation_failure=當前遊戲因為你使用的某些 Mod、光影包、材質包,無法繼續運行。\n請先嘗試禁用你所使用的Mod/光影包/材質包再試。
@@ -450,14 +452,14 @@ game.crash.reason.mod_resolution_missing=當前遊戲因為缺少 Mod 前置,
game.crash.reason.mod_resolution_missing_minecraft=當前遊戲因為 Mod 和 Minecraft 遊戲版本不匹配,無法繼續運行。\n%1$s 需要 Minecraft %2$s 才能運行。\n如果你要繼續使用你已經安裝的 Mod,你可以選擇安裝對應的 Minecraft 版本;如果你要繼續使用當前 Minecraft 版本,你需要安裝對應版本的 Mod。
game.crash.reason.mod_resolution_mod_version=%1$s (版本號 %2$s)
game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
game.crash.reason.forge_repeat_installation=當前遊戲因為 Forge 重複安裝,無法繼續運行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳反饋至 GitHub ,以便我們找到更多線索並修復此問題。\n目前你可以到 自動安裝 裡頭卸載 Forge 並重新安裝。
game.crash.reason.optifine_repeat_installation=當前遊戲因為重複安裝 OptiFine,無法繼續運行。 \n請刪除 Mod 文件夾下的 OptiFine 或前往 遊戲管理-自動安裝 卸載自動安裝的 OptiFine。
game.crash.reason.forge_repeat_installation=當前遊戲因為 Forge 重複安裝,無法繼續運行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳反饋至 GitHub ,以便我們找到更多線索並修復此問題。\n目前你可以到 自動安裝 裡頭移除 Forge 並重新安裝。
game.crash.reason.optifine_repeat_installation=當前遊戲因為重複安裝 OptiFine,無法繼續運行。 \n請刪除 Mod 文件夾下的 OptiFine 或前往 遊戲管理-自動安裝 移除自動安裝的 OptiFine。
game.crash.reason.optifine_is_not_compatible_with_forge=當前遊戲因為OptiFine與當前版本的Forge不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲或在版本設定-自動安裝中更換版本。\n經測試,Forge版本過高或過低都可能導致崩潰。
game.crash.reason.night_config_fixes=當前遊戲因為 Night Config 庫的一些問題,無法繼續運行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://www.github.com/Fuzss/nightconfigfixes">GitHub 倉庫</a>。
game.crash.reason.mod_files_are_decompressed=當前遊戲因為 Mod 檔案被解壓了,無法繼續運行。\n請直接把整個 Mod 檔案放進 Mod 資料夾中即可。\n若解壓就會導致遊戲出錯,請删除Mod資料夾中已被解壓的Mod,然後再啟動遊戲。
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=當前遊戲因為您所安裝的 Mod 過多,超出了遊戲的ID限制,無法繼續運行。\n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修復Mod,或删除部分大型Mod。
game.crash.reason.optifine_causes_the_world_to_fail_to_load=當前遊戲因為 Mod 檔案被解壓了,無法繼續運行。\n請直接把整個Mod檔案放進Mod資料夾中即可!\n若解壓就會導致遊戲出錯,請删除Mod資料夾中已被解壓的Mod,然後再啟動遊戲。
game.crash.reason.modlauncher_8=當前遊戲因為您所使用的 Forge 版本與當前使用的 Java 衝突崩潰,請嘗試更新 Forge。
game.crash.reason.modlauncher_8=當前遊戲因為你所使用的 Forge 版本與當前使用的 Java 衝突崩潰,請嘗試更新 Forge。
game.crash.reason.cannot_find_launch_target_fmlclient=當前遊戲因為 Forge 安裝不完整,無法繼續運行。 \n你可嘗試前往 遊戲管理 - 自動安裝 中選擇 Forge 並重新安裝。
game.crash.reason.shaders_mod=當前遊戲因為同時安裝了 OptiFine 和 Shaders Mod,無法繼續運行。 \n因為 OptiFine 已集成 Shaders Mod 的功能,只需刪除 Shaders Mod 即可。
game.crash.reason.rtss_forest_sodium=當前遊戲因為 RivaTuner Statistics Server (RTSS) 與 Sodium 不相容,導致遊戲崩潰。\n點擊 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此處</a> 查看詳情。
@@ -529,6 +531,36 @@ install.new_game.malformed=名稱無效
install.select=請選擇安裝方式
install.success=安裝成功
java.add=添加 Java
java.add.failed=Java 無效或與目前平臺不相容
java.disable=禁用此 Java
java.disable.confirm=你確定要禁用此 Java 嗎?
java.disabled.management=管理已禁用的 Java
java.disabled.management.remove=從清單中移除此 Java
java.disabled.management.restore=重新啟用此 Java
java.download=下載 Java
java.download.load_list.failed=載入版本清單失敗
java.download.more=更多發行版
java.download.prompt=請選擇你要下載的 Java 版本:
java.download.distribution=發行版
java.download.version=版本
java.download.packageType=包類型
java.management=Java 管理
java.info.architecture=架構
java.info.vendor=供應商
java.info.version=版本
java.info.disco.distribution=發行版本
java.install=安裝 Java
java.install.archive=源路徑
java.install.failed.exists=該名稱已被使用
java.install.failed.invalid=該檔案不是合法的 Java 安裝包,無法繼續安裝。
java.install.failed.unsupported_platform=此 Java 與當前平臺不相容,無法安裝。
java.install.name=名稱
java.install.warning.invalid_character=名稱中包含非法字元
java.reveal=瀏覽 Java 目錄
java.uninstall=移除此 Java
java.uninstall.confirm=你確定要移除此 Java 嗎?此操作無法復原!
lang=正體中文
lang.default=使用系統語言
@@ -536,23 +568,24 @@ launch.advice=%s是否繼續啟動?
launch.advice.multi=檢測到以下問題:\n\n%s\n\n這些問題可能導致遊戲無法正常啟動或影響遊戲體驗,是否繼續啟動?
launch.advice.java.auto=當前選擇的 Java 虛擬機版本不滿足遊戲要求,是否自動選擇合適的 Java 虛擬機版本?或者你可以到遊戲設置中選擇一個合適的 Java 虛擬機版本。
launch.advice.java.modded_java_7=Minecraft 1.7.2 及以下版本需要 Java 7 及以下版本。
launch.advice.corrected=我們已經修正了問題。如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
launch.advice.uncorrected=如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
launch.advice.corrected=我們已經修正了問題。如果確實希望使用你自訂的 Java 虛擬機,你可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
launch.advice.uncorrected=如果你確實希望使用你自訂的 Java 虛擬機,你可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
launch.advice.different_platform=你正在使用 32 位元 Java 啟動遊戲,建議更換至 64 位元 Java。
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容,請更新 Forge 到 2773 或更新的版本。
launch.advice.forge28_2_2_optifine=Forge 28.2.2 或更高版本與 OptiFine 不相容,請将 Forge 降級至 28.2.1 或更低版本。
launch.advice.forge37_0_60=Forge 低於 37.0.60 的版本不相容 Java 17。請更新 Forge 到 37.0.60 或更高版本,或者使用 Java 16 啟動遊戲。
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議你到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod 的 Minecraft 版本不支援 Java 9 或更高版本,請使用 Java 8。
launch.advice.modded_java=部分 Mod 可能與高版本 Java 不相容,建議使用 Java %s 啟動 Minecraft %s。
launch.advice.modlauncher8=您所使用的 Forge 版本與當前使用的 Java 不相容。請嘗試更新 Forge,或使用 Java 8u312/11.0.13/17.0.1 及更早版本啟動。是否繼續啟動?
launch.advice.modlauncher8=你所使用的 Forge 版本與當前使用的 Java 不相容,請更新 Forge。
launch.advice.newer_java=偵測到你正在使用舊版本 Java 啟動遊戲,這可能導致部分 Mod 引發遊戲崩潰,建議更新至 Java 8 後再次啟動。
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。是否繼續啟動?
launch.advice.require_newer_java_version=Minecraft %1$s 僅能運行在 Java %2$s 或更高版本上,但 HMCL 未能找到該 Java 版本,你可以點擊“是”,HMCL 會自動下載他,是否下載?
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。
launch.advice.not_enough_space=你設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。
launch.advice.require_newer_java_version=當前遊戲版本需要 Java %s,但 HMCL 未能找到該 Java 版本,你可以點擊“是”,HMCL 會自動下載他,是否下載?
launch.advice.too_large_memory_for_32bit=你設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。
launch.advice.vanilla_linux_java_8=對於 Linux x86-64 平台,Minecraft 1.12.2 及以下版本與 Java 9+ 不相容,請使用 Java 8 啟動遊戲。
launch.advice.vanilla_x86.translation=Minecraft 尚未為您的平臺提供完善支持,所以可能影響遊戲體驗或無法啟動遊戲。\n你可以在 <a href="https://docs.microsoft.com/java/openjdk/download"> 這裡 </a>下載<b> X86-64 </b> 架構的 Java 以獲得更完整的體驗。\n是否繼續啟動?
launch.advice.vanilla_x86.translation=Minecraft 尚未為你的平臺提供完善支持,所以可能影響遊戲體驗或無法啟動遊戲。\n你可以在 <a href="https://docs.microsoft.com/java/openjdk/download"> 這裡 </a>下載<b> X86-64 </b> 架構的 Java 以獲得更完整的體驗。\n是否繼續啟動?
launch.advice.unknown=由於以下原因,無法繼續啟動遊戲:
launch.failed=啟動失敗
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
launch.failed.creating_process=啟動失敗,在建立新處理程式時發生錯誤。可能是 Java 路徑錯誤。
@@ -562,8 +595,9 @@ launch.failed.download_library=無法下載遊戲相依元件 %s。
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
launch.failed.execution_policy=設定執行策略
launch.failed.execution_policy.failed_to_set=設定執行策略失敗
launch.failed.execution_policy.hint=當前執行策略封锁您執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。
launch.failed.execution_policy.hint=當前執行策略封锁你執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
launch.failed.java_version_too_low=你所指定的 Java 版本過低,請重新設定 Java 版本。
launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java,是否使用默認 Java 啟動遊戲?點擊“是”使用默認 Java 繼續啟動遊戲,\n或者請到遊戲設定中選擇一個合適的Java虛擬機器版本。
launch.failed.sigkill=遊戲被用戶或系統強制終止。
launch.state.dependencies=處理遊戲相依元件
@@ -572,7 +606,7 @@ launch.state.java=檢測 Java 版本
launch.state.logging_in=登入
launch.state.modpack=下載必要檔案
launch.state.waiting_launching=等待遊戲啟動
launch.wrong_javadir=Java 路徑錯誤,將自動重設為預設 Java 路徑。
launch.invalid_java=當前設定的 Java 路徑無效,請重新設定 Java 路徑。
launcher=啟動器
launcher.agreement=用戶協議與免責聲明
@@ -593,9 +627,9 @@ launcher.cache_directory.disabled=停用
launcher.cache_directory.invalid=無法建立自訂的快取目錄,還原至預設設定
launcher.contact=聯絡我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並透過 MCBBS、貼吧、GitHub 或 Minecraft Forum 回報 bug。
launcher.crash.java_internal_error=HHello Minecraft! Launcher 由於當前 Java 損壞而無法繼續運行,請卸載當前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
launcher.crash.java_internal_error=HHello Minecraft! Launcher 由於當前 Java 損壞而無法繼續運行,請移除當前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已偵測到您的啟動器不是最新版本,請更新後重試!
launcher.update_java=請更新您的 Java
launcher.update_java=請更新你的 Java
login.empty_username=你還未設定使用者名稱!
login.enter_password=請輸入您的密碼
@@ -606,10 +640,8 @@ logwindow.title=記錄
logwindow.help=你可以前往 HMCL 社區,尋找他人幫助
logwindow.autoscroll=自動滾動
logwindow.export_game_crash_logs=導出遊戲崩潰訊息
logwindow.export_dump.dependency_ok.button=導出遊戲運行棧
logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒)
logwindow.export_dump.no_dependency.button=導出遊戲運行棧(不兼容)
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。
logwindow.export_dump=導出遊戲運行棧
logwindow.export_dump.no_dependency=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。
main_page=首頁
@@ -670,7 +702,6 @@ modpack.origin.mcbbs.prompt=貼子 id
modpack.scan=解析模組包
modpack.task.install=匯入模組包
modpack.task.install.error=無法識別該模組包,目前僅支援匯入 Curse、Modrinth、MultiMC、MCBBS 模組包。
modpack.task.install.will=將會安裝模組包:
modpack.type.curse=Curse
modpack.type.curse.tolerable_error=但未能完成該模組包檔案的下載,您可以在啟動該遊戲版本時繼續該模組包檔案的下載。由於網路問題,您可能需要重試多次……
modpack.type.curse.error=無法完成該模組包所需的依賴下載,請多次重試或設定代理……
@@ -801,6 +832,8 @@ mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge、Quilt
mods.restore=回退
mods.url=官方頁面
mods.update_modpack_mod.warning=更新模組包中的 Mod 可能導致綜合包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎?
mods.install=安裝到當前實例
mods.save_as=下載到本地目錄
nbt.entries=%s 個條目
nbt.open.failed=打開檔案失敗
@@ -813,6 +846,10 @@ datapack.choose_datapack=選擇要匯入的資料包壓縮檔
datapack.extension=資料包
datapack.title=世界 %s - 資料包
web.failed=加載頁面失敗
web.open_in_browser=是否要在瀏覽器中打開此連結:\n%s
web.view_in_browser=在瀏覽器中查看
world=世界
world.add=加入世界
world.datapack=管理資料包
@@ -981,6 +1018,7 @@ settings.game.java_directory.auto.not_found=沒有合適的 Java
settings.game.java_directory.bit=%s 位
settings.game.java_directory.choose=選擇 Java 路徑
settings.game.java_directory.invalid=Java 路徑不正確
settings.game.java_directory.version=指定 Java 版本
settings.game.java_directory.template=%s(%s)
settings.game.management=管理
settings.game.working_directory=執行路徑(版本隔離,修改後請自行移動相關遊戲檔案,如存檔模組設定等)
@@ -1004,6 +1042,7 @@ settings.launcher.font=字體
settings.launcher.general=通用
settings.launcher.language=語言 (重啟後生效)
settings.launcher.launcher_log.export=匯出啟動器日誌
settings.launcher.launcher_log.reveal=打開日誌目錄
settings.launcher.launcher_log.export.failed=無法匯出日誌
settings.launcher.launcher_log.export.success=日誌已儲存到 %s
settings.launcher.log=記錄
@@ -193,6 +193,7 @@ button.retry=重试
button.save=保存
button.save_as=另存为
button.select_all=全选
button.view=查看
button.yes=是
chat=官方群组
@@ -222,6 +223,7 @@ curse.category.4482=大型整合包
curse.category.4472=科技
curse.category.4473=魔法
curse.category.5128=原版增强
curse.category.7418=恐怖
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5299=教育
@@ -343,6 +345,7 @@ download.provider.official=尽量使用官方源(最新,但可能加载慢
download.provider.balanced=选择加载速度快的下载源(平衡,但可能不是最新)
download.provider.mirror=尽量使用镜像源(加载快,但可能不是最新)
download.java=下载 Java
download.java.override=此 Java 版本已经存在,是否卸载并重新安装?
download.javafx=正在下载必要的运行时组件……
download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\n点击“切换下载源”按钮查看详情以及选择下载源,点击“取消”按钮停止并退出。\n注意:若下载速度过慢,请尝试切换下载源
download.javafx.component=正在下载模块 %s
@@ -361,7 +364,6 @@ extension.png=图片文件
extension.ps1=PowerShell 脚本
extension.sh=Bash 脚本
fatal.fractureiser=Hello Minecraft! Launcher 检测到你的电脑被 Fractureiser 病毒感染,存在严重安全问题。\n请立即使用杀毒软件进行全盘查杀,随后修改你在此电脑上登陆过的所有账户的密码。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHello Minecraft! Launcher 无法在低于 Java 11 的 Java 环境上自行补全 JavaFX 运行环境,请更新到 Java 11 或更高版本。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incomplete=JavaFX 运行环境不完整,请尝试更换你的 Java 或者重新安装 OpenJFX。
fatal.javafx.missing=缺少 JavaFX 运行环境,请使用包含 OpenJFX 的 Java 运行环境启动 Hello Minecraft! Launcher。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
@@ -528,6 +530,36 @@ install.new_game.malformed=名字不合法
install.select=请选择安装方式
install.success=安装成功
java.add=添加 Java
java.add.failed=Java 无效或与当前平台不兼容。
java.disable=禁用此 Java
java.disable.confirm=您确定要禁用此 Java 吗?
java.disabled.management=管理已禁用的 Java
java.disabled.management.remove=从列表中移除此 Java
java.disabled.management.restore=重新启用此 Java
java.download=下载 Java
java.download.load_list.failed=加载版本列表失败
java.download.more=更多发行版
java.download.prompt=请选择你要下载的 Java 版本:
java.download.distribution=发行版
java.download.version=版本
java.download.packageType=包类型
java.management=Java 管理
java.info.architecture=架构
java.info.vendor=供应商
java.info.version=版本
java.info.disco.distribution=发行版
java.install=安装 Java
java.install.archive=源路径
java.install.failed.exists=该名称已被使用
java.install.failed.invalid=该压缩包不是合法的 Java 安装包,无法继续安装。
java.install.failed.unsupported_platform=此 Java 与当前平台不兼容,无法安装。
java.install.name=名称
java.install.warning.invalid_character=名称中包含非法字符
java.reveal=浏览 Java 目录
java.uninstall=卸载此 Java
java.uninstall.confirm=您确定要卸载此 Java 吗?此操作无法撤销!
lang=简体中文
lang.default=跟随系统语言
@@ -545,13 +577,14 @@ launch.advice.java8_1_13=Minecraft 1.13 及以上版本只能运行在 Java 8
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃,建议更新 Java 至 1.8.0_51 或更高版本后再次启动。
launch.advice.java9=低于 1.13 的有安装模组的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.modded_java=部分模组可能与高版本 Java 不兼容,建议使用 Java %s 启动 Minecraft %s。
launch.advice.modlauncher8=您所使用的 Forge 版本与当前使用的 Java 不兼容。请尝试更新 Forge,或使用 Java 8u312/11.0.13/17.0.1 及更早版本启动。
launch.advice.modlauncher8=您所使用的 Forge 版本与当前使用的 Java 不兼容,请更新 Forge。
launch.advice.newer_java=检测到你正在使用旧版本 Java 启动游戏,这可能导致部分模组引发游戏崩溃,建议更新至 Java 8 后再次启动。
launch.advice.not_enough_space=你设置的内存大小过大,超过了系统内存容量 %dMB,可能导致游戏无法启动。
launch.advice.require_newer_java_version=Minecraft %1$s 仅能运行在 Java %2$s 或更高版本上,但 HMCL 未能找到该 Java 版本,你可以点击“是”,HMCL会自动下载他,是否下载?\n如遇到问题,你可以点击右上角帮助按钮进行求助。
launch.advice.require_newer_java_version=当前游戏版本需要 Java %s,但 HMCL 未能找到该 Java 版本,你可以点击“是”,HMCL 会自动下载他,是否下载?\n如遇到问题,你可以点击右上角帮助按钮进行求助。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2 及以下版本与 Java 9+ 不兼容,请使用 Java 8 启动游戏。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
launch.advice.vanilla_x86.translation=Minecraft 尚未为您的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n你可以在 <a href="https://docs.microsoft.com/java/openjdk/download"> 这里 </a> 下载 <b> X86-64 </b> 架构的 Java 以获得更完整的体验。
launch.advice.unknown=由于以下原因,无法继续启动游戏:
launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
@@ -563,6 +596,7 @@ launch.failed.execution_policy=设置执行策略
launch.failed.execution_policy.failed_to_set=设置执行策略失败
launch.failed.execution_policy.hint=当前执行策略阻止您执行 PowerShell 脚本。\n点击“确定”允许当前用户执行本地 PowerShell 脚本,或点击“取消”保持现状。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.failed.java_version_too_low=你所指定的 Java 版本过低,请重新设置 Java 版本。
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java,是否使用默认 Java 启动游戏?点击“是”使用默认 Java 继续启动游戏,\n或者请到全局(特定)游戏设置中选择一个合适的 Java 虚拟机版本。\n你可以点击右上角帮助按钮进行求助。
launch.failed.sigkill=游戏被用户或系统强制终止。
launch.state.dependencies=处理游戏依赖
@@ -571,7 +605,7 @@ launch.state.java=检测 Java 版本
launch.state.logging_in=登录
launch.state.modpack=下载必要文件
launch.state.waiting_launching=等待游戏启动
launch.wrong_javadir=错误的 Java 路径,将自动重置为默认 Java 路径。
launch.invalid_java=当前设置的 Java 路径无效,请重新设置 Java 路径。
launcher=启动器
launcher.agreement=用户协议与免责声明
@@ -605,10 +639,8 @@ logwindow.title=日志
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
logwindow.autoscroll=自动滚动
logwindow.export_game_crash_logs=导出游戏崩溃信息
logwindow.export_dump.dependency_ok.button=导出游戏运行栈
logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒)
logwindow.export_dump.no_dependency.button=导出游戏运行栈(不兼容)
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。
logwindow.export_dump=导出游戏运行栈
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。
main_page=主页
@@ -669,7 +701,6 @@ modpack.origin.mcbbs.prompt=贴子 id
modpack.scan=解析整合包
modpack.task.install=导入整合包
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、Modrinth、MultiMC、MCBBS 整合包。\n你可以点击右上角帮助按钮进行求助。
modpack.task.install.will=导入的整合包文件位置:
modpack.type.curse=Curse
modpack.type.curse.tolerable_error=但未能完成该整合包所需的依赖下载,您可以在启动该游戏版本时继续该整合包文件的下载。由于网络问题,您可能需要重试多次。\n你可以点击右上角帮助按钮进行求助。
modpack.type.curse.error=未能完成该整合包所需的依赖下载,请多次重试或设置代理。\n你可以点击右上角帮助按钮进行求助。
@@ -800,6 +831,8 @@ mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteL
mods.restore=回退
mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
mods.install=安装到当前版本
mods.save_as=下载到本地目录
nbt.entries=%s 个条目
nbt.open.failed=打开文件失败
@@ -812,6 +845,10 @@ datapack.choose_datapack=选择要导入的数据包压缩包
datapack.extension=数据包
datapack.title=世界 %s - 数据包
web.failed=加载页面失败
web.open_in_browser=是否要在浏览器中打开此链接:\n%s
web.view_in_browser=在浏览器中查看
world=世界
world.add=添加世界
world.datapack=管理数据包
@@ -980,6 +1017,7 @@ settings.game.java_directory.auto.not_found=没有合适的 Java
settings.game.java_directory.bit=%s 位
settings.game.java_directory.choose=选择 Java 路径
settings.game.java_directory.invalid=Java 路径不正确
settings.game.java_directory.version=指定 Java 版本
settings.game.java_directory.template=%s(%s)
settings.game.management=管理
settings.game.working_directory=版本隔离(建议使用模组时开启“各版本隔离”,改后需移动存档模组等相关游戏文件)
@@ -1003,6 +1041,7 @@ settings.launcher.font=字体
settings.launcher.general=通用
settings.launcher.language=语言 (重启后生效)
settings.launcher.launcher_log.export=导出启动器日志
settings.launcher.launcher_log.reveal=打开日志目录
settings.launcher.launcher_log.export.failed=无法导出日志
settings.launcher.launcher_log.export.success=日志已保存到 %s
settings.launcher.log=日志
+220
View File
@@ -2265,6 +2265,226 @@
"org.lwjgl:lwjgl-glfw:3.3.1:natives-linux": null,
"org.lwjgl:lwjgl-stb:3.3.1:natives-linux": null,
"org.lwjgl:lwjgl-tinyfd:3.3.1:natives-linux": null,
"org.lwjgl:lwjgl:3.3.2": {
"name": "org.lwjgl:lwjgl:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.3.4/lwjgl-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.3.4/lwjgl-3.3.4.jar",
"sha1": "b86c3e4832426e8a6b466013b7cb34b40e9ce956",
"size": 800127
}
}
},
"org.lwjgl:lwjgl:3.3.2:natives-linux": {
"name": "org.glavo.hmcl:lwjgl3-natives:3.3.4-rc2",
"downloads": {
"classifiers": {
"linux-loongarch64": {
"path": "org/glavo/hmcl/lwjgl3-natives/3.3.4-rc2/lwjgl3-natives-3.3.4-rc2-linux-loongarch64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/hmcl/lwjgl3-natives/3.3.4-rc2-linux-loongarch64/lwjgl3-natives-3.3.4-rc2-linux-loongarch64.jar",
"sha1": "34a7f913c6750f2bede863f59c074cc4d540fb64",
"size": 12234234
}
}
},
"extract": {
"exclude": [
"META-INF/"
]
},
"natives": {
"linux": "linux-loongarch64"
}
},
"org.lwjgl:lwjgl-jemalloc:3.3.2": {
"name": "org.lwjgl:lwjgl-jemalloc:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.3.4/lwjgl-jemalloc-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.3.4/lwjgl-jemalloc-3.3.4.jar",
"sha1": "e3f5dcb8e13f3a5ed3f740fd30a114cee2a80bc4",
"size": 46430
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.3.2:natives-linux": null,
"org.lwjgl:lwjgl-openal:3.3.2": {
"name": "org.lwjgl:lwjgl-openal:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.3.4/lwjgl-openal-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.3.4/lwjgl-openal-3.3.4.jar",
"sha1": "9b74d3ea380c83353d42af43ad9659e04dabe84a",
"size": 113103
}
}
},
"org.lwjgl:lwjgl-openal:3.3.2:natives-linux": null,
"org.lwjgl:lwjgl-opengl:3.3.2": {
"name": "org.lwjgl:lwjgl-opengl:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.3.4/lwjgl-opengl-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.3.4/lwjgl-opengl-3.3.4.jar",
"sha1": "2852ac7d9f6fc71349f1ce28e2708ff1977f18af",
"size": 931960
}
}
},
"org.lwjgl:lwjgl-opengl:3.3.2:natives-linux": null,
"org.lwjgl:lwjgl-glfw:3.3.2": {
"name": "org.lwjgl:lwjgl-glfw:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.3.4/lwjgl-glfw-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.3.4/lwjgl-glfw-3.3.4.jar",
"sha1": "7e46ecdec85db8738053cfde1414352cd62dab74",
"size": 147044
}
}
},
"org.lwjgl:lwjgl-glfw:3.3.2:natives-linux": null,
"org.lwjgl:lwjgl-stb:3.3.2": {
"name": "org.lwjgl:lwjgl-stb:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.3.4/lwjgl-stb-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.3.4/lwjgl-stb-3.3.4.jar",
"sha1": "5821735d5ef23f6da8542887344e57eb181b7cac",
"size": 143112
}
}
},
"org.lwjgl:lwjgl-stb:3.3.2:natives-linux": null,
"org.lwjgl:lwjgl-tinyfd:3.3.2": {
"name": "org.lwjgl:lwjgl-tinyfd:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.3.4/lwjgl-tinyfd-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.3.4/lwjgl-tinyfd-3.3.4.jar",
"sha1": "2d73789ffd8962b38d9d599cc38b2383ce818c7a",
"size": 15928
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.3.2:natives-linux": null,
"org.lwjgl:lwjgl:3.3.3": {
"name": "org.lwjgl:lwjgl:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.3.4/lwjgl-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.3.4/lwjgl-3.3.4.jar",
"sha1": "b86c3e4832426e8a6b466013b7cb34b40e9ce956",
"size": 800127
}
}
},
"org.lwjgl:lwjgl:3.3.3:natives-linux": {
"name": "org.glavo.hmcl:lwjgl3-natives:3.3.4-rc2",
"downloads": {
"classifiers": {
"linux-loongarch64": {
"path": "org/glavo/hmcl/lwjgl3-natives/3.3.4-rc2/lwjgl3-natives-3.3.4-rc2-linux-loongarch64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/hmcl/lwjgl3-natives/3.3.4-rc2-linux-loongarch64/lwjgl3-natives-3.3.4-rc2-linux-loongarch64.jar",
"sha1": "34a7f913c6750f2bede863f59c074cc4d540fb64",
"size": 12234234
}
}
},
"extract": {
"exclude": [
"META-INF/"
]
},
"natives": {
"linux": "linux-loongarch64"
}
},
"org.lwjgl:lwjgl-jemalloc:3.3.3": {
"name": "org.lwjgl:lwjgl-jemalloc:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.3.4/lwjgl-jemalloc-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.3.4/lwjgl-jemalloc-3.3.4.jar",
"sha1": "e3f5dcb8e13f3a5ed3f740fd30a114cee2a80bc4",
"size": 46430
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.3.3:natives-linux": null,
"org.lwjgl:lwjgl-openal:3.3.3": {
"name": "org.lwjgl:lwjgl-openal:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.3.4/lwjgl-openal-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.3.4/lwjgl-openal-3.3.4.jar",
"sha1": "9b74d3ea380c83353d42af43ad9659e04dabe84a",
"size": 113103
}
}
},
"org.lwjgl:lwjgl-openal:3.3.3:natives-linux": null,
"org.lwjgl:lwjgl-opengl:3.3.3": {
"name": "org.lwjgl:lwjgl-opengl:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.3.4/lwjgl-opengl-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.3.4/lwjgl-opengl-3.3.4.jar",
"sha1": "2852ac7d9f6fc71349f1ce28e2708ff1977f18af",
"size": 931960
}
}
},
"org.lwjgl:lwjgl-opengl:3.3.3:natives-linux": null,
"org.lwjgl:lwjgl-glfw:3.3.3": {
"name": "org.lwjgl:lwjgl-glfw:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.3.4/lwjgl-glfw-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.3.4/lwjgl-glfw-3.3.4.jar",
"sha1": "7e46ecdec85db8738053cfde1414352cd62dab74",
"size": 147044
}
}
},
"org.lwjgl:lwjgl-glfw:3.3.3:natives-linux": null,
"org.lwjgl:lwjgl-stb:3.3.3": {
"name": "org.lwjgl:lwjgl-stb:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.3.4/lwjgl-stb-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.3.4/lwjgl-stb-3.3.4.jar",
"sha1": "5821735d5ef23f6da8542887344e57eb181b7cac",
"size": 143112
}
}
},
"org.lwjgl:lwjgl-stb:3.3.3:natives-linux": null,
"org.lwjgl:lwjgl-tinyfd:3.3.3": {
"name": "org.lwjgl:lwjgl-tinyfd:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.3.4/lwjgl-tinyfd-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.3.4/lwjgl-tinyfd-3.3.4.jar",
"sha1": "2d73789ffd8962b38d9d599cc38b2383ce818c7a",
"size": 15928
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.3.3:natives-linux": null,
"org.lwjgl:lwjgl-freetype:3.3.3": {
"name": "org.lwjgl:lwjgl-freetype:3.3.4",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.3.4/lwjgl-freetype-3.3.4.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.3.4/lwjgl-freetype-3.3.4.jar",
"sha1": "23f7bf165068ef2ca80ae1b79fd905af20498600",
"size": 453489
}
}
},
"org.lwjgl:lwjgl-freetype:3.3.3:natives-linux": null,
"net.java.dev.jna:jna:5.8.0": {
"name": "net.java.dev.jna:jna:5.13.0",
"downloads": {
@@ -0,0 +1,314 @@
{
"windows-x86": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "win-x86",
"sha1": "6c9ebafc7f9c4544d72fa5e306f4111e56b5db58"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "win-x86",
"sha1": "a14a1fbe3a0dca81d99c53fd7be8e7c784a68afe"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "win-x86",
"sha1": "7df1501701f9e9fbadab8ce55ef1dde128e2e88a"
}
],
"windows-x86_64": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "win",
"sha1": "f33d0776b2b027c2ea9267c184f9048ef94c4deb"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "win",
"sha1": "951ca77bd50ef4fd44f4b7fb1e38301088fde4e2"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "win",
"sha1": "78ac7de48d0aac48e7d5282efc81bab7f337b587"
}
],
"windows-arm64": [
{
"module": "javafx.base",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-base",
"version": "18.0.2+1-arm64",
"classifier": "win",
"sha1": "4518a696b9d509dc09a7fe283452fce84a1686a8"
},
{
"module": "javafx.graphics",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-graphics",
"version": "18.0.2+1-arm64",
"classifier": "win",
"sha1": "e19ba9aefc4bba8ff86dcdf416620b93b7bdea39"
},
{
"module": "javafx.controls",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-controls",
"version": "18.0.2+1-arm64",
"classifier": "win",
"sha1": "0bf7380823bb8c420dd41837d2c71087b8953ec1"
}
],
"osx-x86_64": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "mac",
"sha1": "73e422d8426aaa23e8c712b9f4d9bebf70d3bfb9"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "mac",
"sha1": "6ab6f3e23421fcfa04e692d9d26a8f55a830c114"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "mac",
"sha1": "b7786b1b63e741c0e234829825fae5fef9d96c31"
}
],
"osx-arm64": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "mac-aarch64",
"sha1": "1d0d887c492330ed527b0614d115a4f32d2d05a4"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "mac-aarch64",
"sha1": "64db28799e61e0f8f51e471c732599b2a6961803"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "mac-aarch64",
"sha1": "3a14bd5f3ebe45d344c1f2bade0fe074e6ea2a83"
}
],
"linux-x86_64": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "linux",
"sha1": "9e4ca5ce2b87c479d2cc445ff411a48aeae76936"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "linux",
"sha1": "27c64e4e1569ddf198f28eda84cf8d014ab80693"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "linux",
"sha1": "b5b2a8ead40bd43476740aa51697bb1cac23de5c"
}
],
"linux-arm32": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "linux-arm32-monocle",
"sha1": "452f455d6948788c1e5350a41259eb8101d3f82a"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "linux-arm32-monocle",
"sha1": "b45b33252e88263fe80a462a45828b4562c3c709"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "linux-arm32-monocle",
"sha1": "e606c619fc493ecd18d281b0ded3aa38ba15a9e5"
}
],
"linux-arm64": [
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1",
"classifier": "linux-aarch64",
"sha1": "1490bfe619e148b3d58746d43f549d697640c935"
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1",
"classifier": "linux-aarch64",
"sha1": "cad8004a87f57d9813c52985894ef15e8011baee"
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1",
"classifier": "linux-aarch64",
"sha1": "ccc33fc1fcbf46130346f4330d6d70b71bdec7d0"
}
],
"linux-loongarch64": [
{
"module": "javafx.base",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-base",
"version": "17.0.8-loongarch64",
"classifier": "linux",
"sha1": "9d692dfc79eb334a7b4bae922aa57cb3a437345e"
},
{
"module": "javafx.graphics",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-graphics",
"version": "17.0.8-loongarch64",
"classifier": "linux",
"sha1": "7f241dc6adc8beddb776f453a3c63a5848d7b90b"
},
{
"module": "javafx.controls",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-controls",
"version": "17.0.8-loongarch64",
"classifier": "linux",
"sha1": "1c8b0141bec93ed21d7c0e9a2f69a1c7e3c734d2"
}
],
"linux-loongarch64_ow": [
{
"module": "javafx.base",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-base",
"version": "19-ea+10-loongson64",
"classifier": "linux",
"sha1": "f90663ba93aef9f818236ce19ecfa33dca0ffe10"
},
{
"module": "javafx.graphics",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-graphics",
"version": "19-ea+10-loongson64",
"classifier": "linux",
"sha1": "6ff76304d08bba093abfe7f4d50ce6a49279c87f"
},
{
"module": "javafx.controls",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-controls",
"version": "19-ea+10-loongson64",
"classifier": "linux",
"sha1": "8a16096d42de70f2548f18840cdcd49a07fc1654"
}
],
"linux-riscv64": [
{
"module": "javafx.base",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-base",
"version": "19.0.2.1-riscv64",
"classifier": "linux",
"sha1": "e33c7e0bac2931a8f7a752bb74e4e4e535eec4ad"
},
{
"module": "javafx.graphics",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-graphics",
"version": "19.0.2.1-riscv64",
"classifier": "linux",
"sha1": "ac4e5edd55d84da80f8fc2d81a4c9994296a6c09"
},
{
"module": "javafx.controls",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-controls",
"version": "19.0.2.1-riscv64",
"classifier": "linux",
"sha1": "efe6a87ea24972d45f1931528f87e64a53bd2232"
}
],
"freebsd-x86_64": [
{
"module": "javafx.base",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-base",
"version": "14.0.2.1-freebsd",
"classifier": "freebsd",
"sha1": "7bac900f0ab0d4d6dcf178252cf37ee1e0470d40"
},
{
"module": "javafx.graphics",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-graphics",
"version": "14.0.2.1-freebsd",
"classifier": "freebsd",
"sha1": "7773ce02d1dc29160801e4e077ed2a26e93bed13"
},
{
"module": "javafx.controls",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-controls",
"version": "14.0.2.1-freebsd",
"classifier": "freebsd",
"sha1": "34a3dd3ccbc898bacfdcb3256cfb87ddc50621d3"
}
]
}
@@ -20,10 +20,11 @@ package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.JavaFXLauncher;
import org.jackhuang.hmcl.game.ClassicVersion;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.Platform;
import org.junit.jupiter.api.Disabled;
@@ -35,8 +36,6 @@ import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Pair.pair;
public class GameCrashWindowTest {
@Test
@@ -53,11 +52,11 @@ public class GameCrashWindowTest {
GameCrashWindow window = new GameCrashWindow(process, ProcessListener.ExitType.APPLICATION_ERROR, null,
new ClassicVersion(),
new LaunchOptions.Builder()
.setJava(new JavaVersion(Paths.get("."), "16", Platform.SYSTEM_PLATFORM))
.setJava(new JavaRuntime(Paths.get("."), new JavaInfo(Platform.SYSTEM_PLATFORM, "16", null), false, false))
.setGameDir(new File("."))
.create(),
Arrays.stream(logs.split("\\n"))
.map(log -> pair(log, Log4jLevel.guessLevel(log)))
.map(Log::new)
.collect(Collectors.toList()));
window.showAndWait();
+1
View File
@@ -12,5 +12,6 @@ dependencies {
api("com.github.steveice10:opennbt:1.5")
api("org.nanohttpd:nanohttpd:2.3.1")
api("org.apache.commons:commons-compress:1.25.0")
api("org.jsoup:jsoup:1.18.1")
compileOnlyApi("org.jetbrains:annotations:24.1.0")
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.auth.offline;
import com.google.gson.reflect.TypeToken;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
@@ -38,6 +37,7 @@ import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public class YggdrasilServer extends HttpServer {
@@ -81,8 +81,7 @@ public class YggdrasilServer extends HttpServer {
}
private Response profiles(Request request) throws IOException {
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
}.getType());
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), listTypeOf(String.class));
return ok(names.stream().distinct()
.map(this::findCharacterByName)
.flatMap(Lang::toStream)
@@ -270,6 +270,13 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
private final Pattern group, artifact;
private final ModLoaderType modLoaderType;
private static final Map<String, LibraryType> PATCH_ID_MAP = new HashMap<>();
static {
for (LibraryType type : values()) {
PATCH_ID_MAP.put(type.getPatchId(), type);
}
}
LibraryType(boolean modLoader, String patchId, Pattern group, Pattern artifact, ModLoaderType modLoaderType) {
this.modLoader = modLoader;
this.patchId = patchId;
@@ -291,10 +298,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
}
public static LibraryType fromPatchId(String patchId) {
for (LibraryType type : values())
if (type.getPatchId().equals(patchId))
return type;
return null;
return PATCH_ID_MAP.get(patchId);
}
protected boolean matchLibrary(Library library, List<Library> libraries) {
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.fabric.FabricAPIVersionList;
import org.jackhuang.hmcl.download.fabric.FabricVersionList;
import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList;
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
import org.jackhuang.hmcl.download.neoforge.NeoForgeOfficialVersionList;
@@ -35,7 +35,7 @@ public class MojangDownloadProvider implements DownloadProvider {
private final GameVersionList game;
private final FabricVersionList fabric;
private final FabricAPIVersionList fabricApi;
private final ForgeBMCLVersionList forge;
private final ForgeVersionList forge;
private final NeoForgeOfficialVersionList neoforge;
private final LiteLoaderVersionList liteLoader;
private final OptiFineBMCLVersionList optifine;
@@ -49,7 +49,7 @@ public class MojangDownloadProvider implements DownloadProvider {
this.game = new GameVersionList(this);
this.fabric = new FabricVersionList(this);
this.fabricApi = new FabricAPIVersionList(this);
this.forge = new ForgeBMCLVersionList(apiRoot);
this.forge = new ForgeVersionList(this);
this.neoforge = new NeoForgeOfficialVersionList(this);
this.liteLoader = new LiteLoaderVersionList(this);
this.optifine = new OptiFineBMCLVersionList(apiRoot);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.download.fabric;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -25,13 +24,13 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
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> {
private final DownloadProvider downloadProvider;
@@ -69,8 +68,8 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
private List<String> getGameVersions(String metaUrl) throws IOException {
String json = NetworkUtils.doGet(downloadProvider.injectURLWithCandidates(metaUrl));
return JsonUtils.GSON.<ArrayList<GameVersion>>fromJson(json, new TypeToken<ArrayList<GameVersion>>() {
}.getType()).stream().map(GameVersion::getVersion).collect(Collectors.toList());
return JsonUtils.GSON.fromJson(json, listTypeOf(GameVersion.class))
.stream().map(GameVersion::getVersion).collect(Collectors.toList());
}
private static String getLaunchMetaUrl(String gameVersion, String loaderVersion) {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.download.forge;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
@@ -40,6 +39,7 @@ 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;
public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion> {
@@ -87,11 +87,9 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
String lookupVersion = toLookupVersion(gameVersion);
return CompletableFuture.completedFuture(null)
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + lookupVersion).<List<ForgeVersion>>getJson(new TypeToken<List<ForgeVersion>>() {
}.getType())))
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + lookupVersion).getJson(listTypeOf(ForgeVersion.class))))
.thenAcceptAsync(forgeVersions -> {
lock.writeLock().lock();
try {
versions.clear(gameVersion);
if (forgeVersions == null) return;
@@ -40,7 +40,7 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
@@ -129,7 +129,7 @@ public class ForgeNewInstallTask extends Task<Version> {
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add(JavaRuntime.getDefault().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
@@ -33,7 +33,7 @@ public final class ForgeVersion implements Validation {
private final String jobver;
private final String version;
private final int build;
private final double modified;
private final long modified;
private final String[][] files;
/**
@@ -44,7 +44,7 @@ public final class ForgeVersion implements Validation {
this(null, null, null, null, 0, 0, null);
}
public ForgeVersion(String branch, String mcversion, String jobver, String version, int build, double modified, String[][] files) {
public ForgeVersion(String branch, String mcversion, String jobver, String version, int build, long modified, String[][] files) {
this.branch = branch;
this.mcversion = mcversion;
this.jobver = jobver;
@@ -74,7 +74,7 @@ public final class ForgeVersion implements Validation {
return build;
}
public double getModified() {
public long getModified() {
return modified;
}
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -43,9 +44,17 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
return false;
}
private static String toLookupVersion(String gameVersion) {
return "1.7.10-pre4".equals(gameVersion) ? "1.7.10_pre4" : gameVersion;
}
private static String fromLookupVersion(String lookupVersion) {
return "1.7.10_pre4".equals(lookupVersion) ? "1.7.10-pre4" : lookupVersion;
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(downloadProvider.injectURL(FORGE_LIST)).getJsonAsync(ForgeVersionRoot.class)
return HttpRequest.GET(FORGE_LIST).getJsonAsync(ForgeVersionRoot.class)
.thenAcceptAsync(root -> {
lock.writeLock().lock();
@@ -55,7 +64,7 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
versions.clear();
for (Map.Entry<String, int[]> entry : root.getGameVersions().entrySet()) {
String gameVersion = VersionNumber.normalize(entry.getKey());
String gameVersion = fromLookupVersion(VersionNumber.normalize(entry.getKey()));
for (int v : entry.getValue()) {
ForgeVersion version = root.getNumber().get(v);
if (version == null)
@@ -71,8 +80,12 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
if (jar == null)
continue;
versions.put(gameVersion, new ForgeRemoteVersion(
version.getGameVersion(), version.getVersion(), null, Collections.singletonList(jar)
toLookupVersion(version.getGameVersion()),
version.getVersion(),
version.getModified() > 0 ? Instant.ofEpochSecond(version.getModified()) : null,
Collections.singletonList(jar)
));
}
}
@@ -82,5 +95,5 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
});
}
public static final String FORGE_LIST = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/json";
public static final String FORGE_LIST = "https://hmcl-dev.github.io/metadata/forge/";
}
@@ -0,0 +1,36 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.platform.Platform;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Glavo
*/
public interface JavaDistribution<V extends JavaRemoteVersion> {
String getDisplayName();
Set<JavaPackageType> getSupportedPackageTypes();
Task<TreeMap<Integer, V>> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType);
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,24 +15,30 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
package org.jackhuang.hmcl.download.java;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public enum JavaPackageType {
JDK(true, false),
JRE(false, false),
JDKFX(true, true),
JREFX(false, true);
public enum LoadingState {
DEPENDENCIES("launch.state.dependencies"),
MODS("launch.state.modpack"),
LOGGING_IN("launch.state.logging_in"),
LAUNCHING("launch.state.waiting_launching"),
DONE("launch.state.done");
private final boolean jdk;
private final boolean javafx;
private final String key;
LoadingState(String key) {
this.key = key;
JavaPackageType(boolean jdk, boolean javafx) {
this.jdk = jdk;
this.javafx = javafx;
}
public String getLocalizedMessage() {
return i18n(key);
public boolean isJDK() {
return jdk;
}
public boolean isJavaFXBundled() {
return javafx;
}
}
@@ -0,0 +1,29 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java;
/**
* @author Glavo
*/
public interface JavaRemoteVersion {
int getJdkVersion();
String getJavaVersion();
String getDistributionVersion();
}
@@ -1,168 +0,0 @@
package org.jackhuang.hmcl.download.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class JavaRepository {
private JavaRepository() {
}
public static Task<JavaVersion> downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
return new JavaDownloadTask(javaVersion, getJavaStoragePath(), downloadProvider)
.thenSupplyAsync(() -> {
String platform = getSystemJavaPlatform().orElseThrow(JavaDownloadTask.UnsupportedPlatformException::new);
return addJava(getJavaHome(javaVersion, platform));
});
}
public static JavaVersion addJava(Path javaHome) throws InterruptedException, IOException {
if (Files.isDirectory(javaHome)) {
Path executable = JavaVersion.getExecutable(javaHome);
if (Files.isRegularFile(executable)) {
JavaVersion javaVersion = JavaVersion.fromExecutable(executable);
JavaVersion.getJavas().add(javaVersion);
return javaVersion;
}
}
throw new IOException("Incorrect java home " + javaHome);
}
public static Stream<Optional<Path>> findMinecraftRuntimeDirs() {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
return Stream.of(
FileUtils.tryGetPath(System.getenv("localappdata"),
"Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime"),
FileUtils.tryGetPath(
Optional.ofNullable(System.getenv("ProgramFiles(x86)")).orElse("C:\\Program Files (x86)"),
"Minecraft Launcher\\runtime"));
case LINUX:
case FREEBSD:
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), ".minecraft/runtime"));
case OSX:
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"));
default:
return Stream.empty();
}
}
public static Stream<Path> findJavaHomeInMinecraftRuntimeDir(Path runtimeDir) {
if (!Files.isDirectory(runtimeDir))
return Stream.empty();
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
List<Path> javaHomes = new ArrayList<>();
Consumer<String> action = platform -> {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(runtimeDir)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
findJavaHomeInComponentDir(platform, component).ifPresent(javaHomes::add);
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + runtimeDir, e);
}
};
getSystemJavaPlatform().ifPresent(action);
// Workaround, which will be removed in the future
if (Platform.SYSTEM_PLATFORM == Platform.OSX_ARM64)
action.accept("mac-os-arm64");
return javaHomes.stream();
}
private static Optional<Path> findJavaHomeInComponentDir(String platform, Path component) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return Optional.empty();
Path dir = component.resolve(platform).resolve(component.getFileName());
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath))
return Optional.of(macPath);
else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
return Optional.of(dir);
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return Optional.empty();
}
}
public static Optional<String> getSystemJavaPlatform() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return Optional.of("linux-i386");
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return Optional.of("linux");
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 || Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return Optional.of("mac-os");
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return Optional.of("windows-x86");
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return Optional.of("windows-x64");
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
return Optional.of("windows-x64");
} else {
return Optional.of("windows-x86");
}
}
}
return Optional.empty();
}
public static Path getJavaStoragePath() {
return CacheRepository.getInstance().getCacheDirectory().resolve("java");
}
public static Path getJavaHome(GameJavaVersion javaVersion, String platform) {
Path javaHome = getJavaStoragePath().resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent());
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
javaHome = javaHome.resolve("jre.bundle/Contents/Home");
return javaHome;
}
}
@@ -0,0 +1,95 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.disco;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaPackageType;
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.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.*;
/**
* @author Glavo
*/
public final class DiscoFetchJavaListTask extends Task<TreeMap<Integer, DiscoJavaRemoteVersion>> {
public static final String API_ROOT = System.getProperty("hmcl.discoapi.override", "https://api.foojay.io/disco/v3.0");
private static String getOperatingSystemName(OperatingSystem os) {
return os == OperatingSystem.OSX ? "macos" : os.getCheckedName();
}
private static String getArchitectureName(Architecture arch) {
return arch.getCheckedName();
}
private final DiscoJavaDistribution distribution;
private final Task<String> fetchPackagesTask;
public DiscoFetchJavaListTask(DownloadProvider downloadProvider, DiscoJavaDistribution distribution, Platform platform, JavaPackageType packageType) {
this.distribution = distribution;
HashMap<String, String> params = new HashMap<>();
params.put("distribution", distribution.getApiParameter());
params.put("package", packageType.isJDK() ? "jdk" : "jre");
params.put("javafx_bundled", Boolean.toString(packageType.isJavaFXBundled()));
params.put("operating_system", getOperatingSystemName(platform.getOperatingSystem()));
params.put("architecture", getArchitectureName(platform.getArchitecture()));
params.put("archive_type", platform.getOperatingSystem() == OperatingSystem.WINDOWS ? "zip" : "tar.gz");
params.put("directly_downloadable", "true");
if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
params.put("lib_c_type", "glibc");
}
this.fetchPackagesTask = new GetTask(downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(API_ROOT + "/packages", params)));
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(fetchPackagesTask);
}
@Override
public void execute() throws Exception {
String json = fetchPackagesTask.getResult();
List<DiscoJavaRemoteVersion> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoJavaRemoteVersion.class)).getResult();
TreeMap<Integer, DiscoJavaRemoteVersion> map = new TreeMap<>();
for (DiscoJavaRemoteVersion version : result) {
if (!distribution.getApiParameter().equals(version.getDistribution()))
continue;
int jdkVersion = version.getJdkVersion();
DiscoJavaRemoteVersion oldVersion = map.get(jdkVersion);
if (oldVersion == null || VersionNumber.compare(version.getDistributionVersion(), oldVersion.getDistributionVersion()) > 0) {
map.put(jdkVersion, version);
}
}
setResult(map);
}
}
@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.disco;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.util.*;
import static org.jackhuang.hmcl.download.java.JavaPackageType.*;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.platform.Architecture.*;
import static org.jackhuang.hmcl.util.platform.OperatingSystem.*;
/**
* @author Glavo
*/
public enum DiscoJavaDistribution implements JavaDistribution<DiscoJavaRemoteVersion> {
TEMURIN("Eclipse Temurin", "temurin", "Adoptium",
EnumSet.of(JDK, JRE),
pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64, PPC64LE, S390X, SPARCV9)),
pair(OSX, EnumSet.of(X86_64, ARM64))),
LIBERICA("Liberica", "liberica", "BellSoft",
EnumSet.of(JDK, JRE, JDKFX, JREFX),
pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)),
pair(OSX, EnumSet.of(X86_64, ARM64))),
ZULU("Zulu", "zulu", "Azul",
EnumSet.of(JDK, JRE, JDKFX, JREFX),
pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)),
pair(OSX, EnumSet.of(X86_64, ARM64))),
GRAALVM("GraalVM", "graalvm", "Oracle",
EnumSet.of(JDK),
pair(WINDOWS, EnumSet.of(X86_64, X86)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)),
pair(OSX, EnumSet.of(X86_64, ARM64)));
public static DiscoJavaDistribution of(String name) {
for (DiscoJavaDistribution distribution : values()) {
if (distribution.apiParameter.equalsIgnoreCase(name) || distribution.name().equalsIgnoreCase(name)) {
return distribution;
}
}
return null;
}
private final String displayName;
private final String apiParameter;
private final String vendor;
private final Set<JavaPackageType> supportedPackageTypes;
private final Map<OperatingSystem, EnumSet<Architecture>> supportedPlatforms = new EnumMap<>(OperatingSystem.class);
@SafeVarargs
DiscoJavaDistribution(String displayName, String apiParameter, String vendor, Set<JavaPackageType> supportedPackageTypes, Pair<OperatingSystem, EnumSet<Architecture>>... supportedPlatforms) {
this.displayName = displayName;
this.apiParameter = apiParameter;
this.vendor = vendor;
this.supportedPackageTypes = supportedPackageTypes;
for (Pair<OperatingSystem, EnumSet<Architecture>> platform : supportedPlatforms) {
this.supportedPlatforms.put(platform.getKey(), platform.getValue());
}
}
@Override
public String getDisplayName() {
return displayName;
}
public String getApiParameter() {
return apiParameter;
}
public String getVendor() {
return vendor;
}
@Override
public Set<JavaPackageType> getSupportedPackageTypes() {
return supportedPackageTypes;
}
public boolean isSupport(Platform platform) {
EnumSet<Architecture> architectures = supportedPlatforms.get(platform.getOperatingSystem());
return architectures != null && architectures.contains(platform.getArchitecture());
}
@Override
public Task<TreeMap<Integer, DiscoJavaRemoteVersion>> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType) {
return new DiscoFetchJavaListTask(provider, this, platform, packageType);
}
}
@@ -0,0 +1,259 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.disco;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.util.gson.JsonUtils;
/**
* @author Glavo
*/
public final class DiscoJavaRemoteVersion implements JavaRemoteVersion {
@SerializedName("id")
private final String id;
@SerializedName("archive_type")
private final String archiveType;
@SerializedName("distribution")
private final String distribution;
@SerializedName("major_version")
private final int majorVersion;
@SerializedName("java_version")
private final String javaVersion;
@SerializedName("distribution_version")
private final String distributionVersion;
@SerializedName("jdk_version")
private final int jdkVersion;
@SerializedName("latest_build_available")
private final boolean latestBuildAvailable;
@SerializedName("release_status")
private final String releaseStatus;
@SerializedName("term_of_support")
private final String termOfSupport;
@SerializedName("operating_system")
private final String operatingSystem;
@SerializedName("lib_c_type")
private final String libCType;
@SerializedName("architecture")
private final String architecture;
@SerializedName("fpu")
private final String fpu;
@SerializedName("package_type")
private final String packageType;
@SerializedName("javafx_bundled")
private final boolean javafxBundled;
@SerializedName("directly_downloadable")
private final boolean directlyDownloadable;
@SerializedName("filename")
private final String fileName;
@SerializedName("links")
private final Links links;
@SerializedName("free_use_in_production")
private final boolean freeUseInProduction;
@SerializedName("tck_tested")
private final String tckTested;
@SerializedName("tck_cert_uri")
private final String tckCertUri;
@SerializedName("aqavit_certified")
private final String aqavitCertified;
@SerializedName("aqavit_cert_uri")
private final String aqavitCertUri;
@SerializedName("size")
private final long size;
public DiscoJavaRemoteVersion(String id, String archiveType, String distribution, int majorVersion, String javaVersion, String distributionVersion, int jdkVersion, boolean latestBuildAvailable, String releaseStatus, String termOfSupport, String operatingSystem, String libCType, String architecture, String fpu, String packageType, boolean javafxBundled, boolean directlyDownloadable, String fileName, Links links, boolean freeUseInProduction, String tckTested, String tckCertUri, String aqavitCertified, String aqavitCertUri, long size) {
this.id = id;
this.archiveType = archiveType;
this.distribution = distribution;
this.majorVersion = majorVersion;
this.javaVersion = javaVersion;
this.distributionVersion = distributionVersion;
this.jdkVersion = jdkVersion;
this.latestBuildAvailable = latestBuildAvailable;
this.releaseStatus = releaseStatus;
this.termOfSupport = termOfSupport;
this.operatingSystem = operatingSystem;
this.libCType = libCType;
this.architecture = architecture;
this.fpu = fpu;
this.packageType = packageType;
this.javafxBundled = javafxBundled;
this.directlyDownloadable = directlyDownloadable;
this.fileName = fileName;
this.links = links;
this.freeUseInProduction = freeUseInProduction;
this.tckTested = tckTested;
this.tckCertUri = tckCertUri;
this.aqavitCertified = aqavitCertified;
this.aqavitCertUri = aqavitCertUri;
this.size = size;
}
public String getId() {
return id;
}
public String getArchiveType() {
return archiveType;
}
public String getDistribution() {
return distribution;
}
public int getMajorVersion() {
return majorVersion;
}
@Override
public String getJavaVersion() {
return javaVersion;
}
@Override
public String getDistributionVersion() {
return distributionVersion;
}
@Override
public int getJdkVersion() {
return jdkVersion;
}
public boolean isLatestBuildAvailable() {
return latestBuildAvailable;
}
public String getReleaseStatus() {
return releaseStatus;
}
public String getTermOfSupport() {
return termOfSupport;
}
public String getOperatingSystem() {
return operatingSystem;
}
public String getLibCType() {
return libCType;
}
public String getArchitecture() {
return architecture;
}
public String getFpu() {
return fpu;
}
public String getPackageType() {
return packageType;
}
public boolean isJavafxBundled() {
return javafxBundled;
}
public boolean isDirectlyDownloadable() {
return directlyDownloadable;
}
public String getFileName() {
return fileName;
}
public Links getLinks() {
return links;
}
public boolean isFreeUseInProduction() {
return freeUseInProduction;
}
public String getTckTested() {
return tckTested;
}
public String getTckCertUri() {
return tckCertUri;
}
public String getAqavitCertified() {
return aqavitCertified;
}
public String getAqavitCertUri() {
return aqavitCertUri;
}
public long getSize() {
return size;
}
@Override
public String toString() {
return "DiscoJavaRemoteVersion " + JsonUtils.GSON.toJson(this);
}
public static final class Links {
@SerializedName("pkg_info_uri")
private final String pkgInfoUri;
@SerializedName("pkg_download_redirect")
private final String pkgDownloadRedirect;
public Links(String pkgInfoUri, String pkgDownloadRedirect) {
this.pkgInfoUri = pkgInfoUri;
this.pkgDownloadRedirect = pkgDownloadRedirect;
}
public String getPkgInfoUri() {
return pkgInfoUri;
}
public String getPkgDownloadRedirect() {
return pkgDownloadRedirect;
}
}
}
@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.disco;
import com.google.gson.annotations.SerializedName;
/**
* @author Glavo
*/
public final class DiscoRemoteFileInfo {
@SerializedName("filename")
private final String fileName;
@SerializedName("direct_download_uri")
private final String directDownloadUri;
@SerializedName("checksum_type")
private final String checksumType;
@SerializedName("checksum")
private final String checksum;
@SerializedName("checksum_uri")
private final String checksumUri;
public DiscoRemoteFileInfo(String fileName, String directDownloadUri, String checksumType, String checksum, String checksumUri) {
this.fileName = fileName;
this.directDownloadUri = directDownloadUri;
this.checksumType = checksumType;
this.checksum = checksum;
this.checksumUri = checksumUri;
}
public String getFileName() {
return fileName;
}
public String getDirectDownloadUri() {
return directDownloadUri;
}
public String getChecksumType() {
return checksumType;
}
public String getChecksum() {
return checksum;
}
public String getChecksumUri() {
return checksumUri;
}
}
@@ -0,0 +1,49 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.disco;
import com.google.gson.reflect.TypeToken;
import java.util.List;
/**
* @author Glavo
*/
public final class DiscoResult<T> {
@SuppressWarnings("unchecked")
public static <T> TypeToken<DiscoResult<T>> typeOf(Class<T> argType) {
return (TypeToken<DiscoResult<T>>) TypeToken.getParameterized(DiscoResult.class, argType);
}
private final List<T> result;
private final String message;
private DiscoResult(List<T> result, String message) {
this.result = result;
this.message = message;
}
public List<T> getResult() {
return result;
}
public String getMessage() {
return message;
}
}
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.mojang;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.platform.Platform;
import java.util.Collections;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Glavo
*/
public final class MojangJavaDistribution implements JavaDistribution<JavaRemoteVersion> {
public static final MojangJavaDistribution DISTRIBUTION = new MojangJavaDistribution();
private MojangJavaDistribution() {
}
@Override
public String getDisplayName() {
return "Mojang";
}
@Override
public Set<JavaPackageType> getSupportedPackageTypes() {
return Collections.singleton(JavaPackageType.JRE);
}
@Override
public Task<TreeMap<Integer, JavaRemoteVersion>> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType) {
return null;
}
}
@@ -15,20 +15,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java;
package org.jackhuang.hmcl.download.java.mojang;
import org.jackhuang.hmcl.download.ArtifactMalformedException;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.java.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
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.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.tukaani.xz.LZMAInputStream;
import java.io.File;
@@ -40,50 +39,39 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class JavaDownloadTask extends Task<Void> {
private final GameJavaVersion javaVersion;
private final Path rootDir;
private String platform;
private final Task<RemoteFiles> javaDownloadsTask;
private JavaDownloads.JavaDownload download;
private final List<Task<?>> dependencies = new ArrayList<>();
private final DownloadProvider downloadProvider;
public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Result> {
public JavaDownloadTask(GameJavaVersion javaVersion, Path rootDir, DownloadProvider downloadProvider) {
this.javaVersion = javaVersion;
this.rootDir = rootDir;
private final DownloadProvider downloadProvider;
private final Path target;
private final Task<MojangJavaRemoteFiles> javaDownloadsTask;
private final List<Task<?>> dependencies = new ArrayList<>();
private volatile MojangJavaDownloads.JavaDownload download;
public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, GameJavaVersion javaVersion, String platform) {
this.target = target;
this.downloadProvider = downloadProvider;
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
.thenComposeAsync(javaDownloadsJson -> {
JavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, JavaDownloads.class);
if (!allDownloads.getDownloads().containsKey(platform)) throw new UnsupportedPlatformException();
Map<String, List<JavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
if (!osDownloads.containsKey(javaVersion.getComponent())) throw new UnsupportedPlatformException();
List<JavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.getComponent());
for (JavaDownloads.JavaDownload download : candidates) {
if (VersionNumber.compare(download.getVersion().getName(), Integer.toString(javaVersion.getMajorVersion())) >= 0) {
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);
Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
if (osDownloads == null || !osDownloads.containsKey(javaVersion.getComponent()))
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.getComponent());
for (MojangJavaDownloads.JavaDownload download : candidates) {
if (JavaInfo.parseVersion(download.getVersion().getName()) >= javaVersion.getMajorVersion()) {
this.download = download;
return new GetTask(downloadProvider.injectURLWithCandidates(download.getManifest().getUrl()));
}
}
throw new UnsupportedPlatformException();
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
})
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, RemoteFiles.class));
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
this.platform = JavaRepository.getSystemJavaPlatform().orElseThrow(UnsupportedPlatformException::new);
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
}
@Override
@@ -93,11 +81,10 @@ public class JavaDownloadTask extends Task<Void> {
@Override
public void execute() throws Exception {
Path jvmDir = rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent());
for (Map.Entry<String, RemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
Path dest = jvmDir.resolve(entry.getKey());
if (entry.getValue() instanceof RemoteFiles.RemoteFile) {
RemoteFiles.RemoteFile file = ((RemoteFiles.RemoteFile) entry.getValue());
for (Map.Entry<String, MojangJavaRemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
Path dest = target.resolve(entry.getKey());
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile) {
MojangJavaRemoteFiles.RemoteFile file = ((MojangJavaRemoteFiles.RemoteFile) entry.getValue());
// Use local file if it already exists
try {
@@ -115,11 +102,11 @@ public class JavaDownloadTask extends Task<Void> {
if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma");
File tempFile = jvmDir.resolve(entry.getKey() + ".lzma").toFile();
File tempFile = target.resolve(entry.getKey() + ".lzma").toFile();
FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey());
dependencies.add(task.thenRunAsync(() -> {
Path decompressed = jvmDir.resolve(entry.getKey() + ".tmp");
Path decompressed = target.resolve(entry.getKey() + ".tmp");
try (LZMAInputStream input = new LZMAInputStream(new FileInputStream(tempFile))) {
Files.copy(input, decompressed, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
@@ -144,10 +131,10 @@ public class JavaDownloadTask extends Task<Void> {
} else {
continue;
}
} else if (entry.getValue() instanceof RemoteFiles.RemoteDirectory) {
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteDirectory) {
Files.createDirectories(dest);
} else if (entry.getValue() instanceof RemoteFiles.RemoteLink) {
RemoteFiles.RemoteLink link = ((RemoteFiles.RemoteLink) entry.getValue());
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink) {
MojangJavaRemoteFiles.RemoteLink link = ((MojangJavaRemoteFiles.RemoteLink) entry.getValue());
Files.deleteIfExists(dest);
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
}
@@ -166,16 +153,16 @@ public class JavaDownloadTask extends Task<Void> {
@Override
public void postExecute() throws Exception {
FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(".version").toFile(), download.getVersion().getName());
FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent() + ".sha1").toFile(),
javaDownloadsTask.getResult().getFiles().entrySet().stream()
.filter(entry -> entry.getValue() instanceof RemoteFiles.RemoteFile)
.map(entry -> {
RemoteFiles.RemoteFile file = (RemoteFiles.RemoteFile) entry.getValue();
return entry.getKey() + " /#// " + file.getDownloads().get("raw").getSha1() + " " + file.getDownloads().get("raw").getSize();
})
.collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
setResult(new Result(download, javaDownloadsTask.getResult()));
}
public static class UnsupportedPlatformException extends Exception {}
public static final class Result {
public final MojangJavaDownloads.JavaDownload download;
public final MojangJavaRemoteFiles remoteFiles;
public Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
this.download = download;
this.remoteFiles = remoteFiles;
}
}
}
@@ -15,11 +15,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java;
package org.jackhuang.hmcl.download.java.mojang;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.util.Immutable;
@@ -27,12 +26,15 @@ import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
@JsonAdapter(JavaDownloads.Adapter.class)
public class JavaDownloads {
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
@JsonAdapter(MojangJavaDownloads.Adapter.class)
public class MojangJavaDownloads {
private final Map<String, Map<String, List<JavaDownload>>> downloads;
public JavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
public MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
this.downloads = downloads;
}
@@ -40,17 +42,16 @@ public class JavaDownloads {
return downloads;
}
public static class Adapter implements JsonSerializer<JavaDownloads>, JsonDeserializer<JavaDownloads> {
public static class Adapter implements JsonSerializer<MojangJavaDownloads>, JsonDeserializer<MojangJavaDownloads> {
@Override
public JsonElement serialize(JavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
public JsonElement serialize(MojangJavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.downloads);
}
@Override
public JavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new JavaDownloads(context.deserialize(json, new TypeToken<Map<String, Map<String, List<JavaDownload>>>>() {
}.getType()));
public MojangJavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new MojangJavaDownloads(context.deserialize(json, mapTypeOf(String.class, mapTypeOf(String.class, listTypeOf(JavaDownload.class))).getType()));
}
}
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java;
package org.jackhuang.hmcl.download.java.mojang;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.util.gson.JsonSubtype;
@@ -24,10 +24,10 @@ import org.jackhuang.hmcl.util.gson.JsonType;
import java.util.Collections;
import java.util.Map;
public class RemoteFiles {
public final class MojangJavaRemoteFiles {
private final Map<String, Remote> files;
public RemoteFiles(Map<String, Remote> files) {
public MojangJavaRemoteFiles(Map<String, Remote> files) {
this.files = files;
}
@@ -0,0 +1,56 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.java.mojang;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.game.GameJavaVersion;
/**
* @author Glavo
*/
public final class MojangJavaRemoteVersion implements JavaRemoteVersion {
private final GameJavaVersion gameJavaVersion;
public MojangJavaRemoteVersion(GameJavaVersion gameJavaVersion) {
this.gameJavaVersion = gameJavaVersion;
}
public GameJavaVersion getGameJavaVersion() {
return gameJavaVersion;
}
@Override
public int getJdkVersion() {
return gameJavaVersion.getMajorVersion();
}
@Override
public String getJavaVersion() {
return String.valueOf(getJdkVersion());
}
@Override
public String getDistributionVersion() {
return String.valueOf(getJdkVersion());
}
@Override
public String toString() {
return "MojangJavaRemoteVersion[gameJavaVersion=" + gameJavaVersion + "]";
}
}
@@ -19,18 +19,17 @@ package org.jackhuang.hmcl.download.neoforge;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.HttpRequest;
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.gson.JsonUtils.listTypeOf;
public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVersion> {
private final String apiRoot;
@@ -68,8 +67,7 @@ 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).<List<NeoForgeVersion>>getJson(new TypeToken<List<NeoForgeVersion>>() {
}.getType())))
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/neoforge/list/" + gameVersion).getJson(listTypeOf(NeoForgeVersion.class))))
.thenAcceptAsync(neoForgeVersions -> {
lock.writeLock().lock();
@@ -36,7 +36,7 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
@@ -125,7 +125,7 @@ public class NeoForgeOldInstallTask extends Task<Version> {
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add(JavaRuntime.getDefault().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.download.optifine;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
@@ -26,10 +25,11 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
/**
* @author huangyuhui
*/
@@ -72,8 +72,7 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(apiRoot + "/optifine/versionlist").<List<OptiFineVersion>>getJsonAsync(new TypeToken<List<OptiFineVersion>>() {
}.getType()).thenAcceptAsync(root -> {
return HttpRequest.GET(apiRoot + "/optifine/versionlist").getJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
lock.writeLock().lock();
try {
@@ -27,7 +27,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jenkinsci.constant_pool_scanner.ConstantPool;
@@ -144,7 +144,7 @@ public final class OptiFineInstallTask extends Task<Version> {
Path optiFineLibraryPath = gameRepository.getLibraryFile(version, optiFineLibrary).toPath();
if (Files.exists(fs.getPath("optifine/Patcher.class"))) {
String[] command = {
JavaVersion.fromCurrentEnvironment().getBinary().toString(),
JavaRuntime.getDefault().getBinary().toString(),
"-cp",
dest.toString(),
"optifine.Patcher",
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.download.quilt;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -25,13 +24,13 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
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> {
private final DownloadProvider downloadProvider;
@@ -69,8 +68,8 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
private List<String> getGameVersions(String metaUrl) throws IOException {
String json = NetworkUtils.doGet(downloadProvider.injectURLWithCandidates(metaUrl));
return JsonUtils.GSON.<ArrayList<GameVersion>>fromJson(json, new TypeToken<ArrayList<GameVersion>>() {
}.getType()).stream().map(GameVersion::getVersion).collect(Collectors.toList());
return JsonUtils.GSON.fromJson(json, listTypeOf(GameVersion.class))
.stream().map(GameVersion::getVersion).collect(Collectors.toList());
}
private static String getLaunchMetaUrl(String gameVersion, String loaderVersion) {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.event.*;
@@ -499,18 +498,16 @@ public class DefaultGameRepository implements GameRepository {
* read modpack configuration for a version.
*
* @param version version installed as modpack
* @param <M> manifest type of ModpackConfiguration
* @return modpack configuration object, or null if this version is not a modpack.
* @throws VersionNotFoundException if version does not exist.
* @throws IOException if an i/o error occurs.
*/
@Nullable
public <M> ModpackConfiguration<M> readModpackConfiguration(String version) throws IOException, VersionNotFoundException {
public ModpackConfiguration<?> readModpackConfiguration(String version) throws IOException, VersionNotFoundException {
if (!hasVersion(version)) throw new VersionNotFoundException(version);
File file = getModpackConfiguration(version);
if (!file.exists()) return null;
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<M>>() {
}.getType());
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
}
public boolean isModpack(String version) {
@@ -17,7 +17,91 @@
*/
package org.jackhuang.hmcl.game;
public class GameJavaVersion {
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.*;
public final class GameJavaVersion {
public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21);
public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17);
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
public static final GameJavaVersion LATEST = JAVA_21;
public static GameJavaVersion getMinimumJavaVersion(GameVersionNumber gameVersion) {
if (gameVersion.compareTo("1.21") >= 0)
return JAVA_21;
if (gameVersion.compareTo("1.18") >= 0)
return JAVA_17;
if (gameVersion.compareTo("1.17") >= 0)
return JAVA_16;
if (gameVersion.compareTo("1.13") >= 0)
return JAVA_8;
return null;
}
public static GameJavaVersion get(int major) {
switch (major) {
case 8:
return JAVA_8;
case 16:
return JAVA_16;
case 17:
return JAVA_17;
case 21:
return JAVA_21;
default:
return null;
}
}
public static boolean isSupportedPlatform(Platform platform) {
OperatingSystem os = platform.getOperatingSystem();
Architecture arch = platform.getArchitecture();
switch (arch) {
case X86:
return os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX;
case X86_64:
return os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.OSX;
case ARM64:
return os == OperatingSystem.WINDOWS || os == OperatingSystem.OSX;
default:
return false;
}
}
public static List<GameJavaVersion> getSupportedVersions(Platform platform) {
OperatingSystem operatingSystem = platform.getOperatingSystem();
Architecture architecture = platform.getArchitecture();
if (architecture == Architecture.X86) {
switch (operatingSystem) {
case WINDOWS:
return Arrays.asList(JAVA_8, JAVA_16, JAVA_17);
case LINUX:
return Collections.singletonList(JAVA_8);
}
} else if (architecture == Architecture.X86_64) {
switch (operatingSystem) {
case WINDOWS:
case LINUX:
case OSX:
return Arrays.asList(JAVA_8, JAVA_16, JAVA_17, JAVA_21);
}
} else if (architecture == Architecture.ARM64) {
switch (operatingSystem) {
case WINDOWS:
case OSX:
return Arrays.asList(JAVA_17, JAVA_21);
}
}
return Collections.emptyList();
}
private final String component;
private final int majorVersion;
@@ -38,8 +122,16 @@ public class GameJavaVersion {
return majorVersion;
}
public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21);
public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17);
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
@Override
public int hashCode() {
return getMajorVersion();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GameJavaVersion)) return false;
GameJavaVersion that = (GameJavaVersion) o;
return majorVersion == that.majorVersion;
}
}
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff. Show more