Compare commits

..
39 Commits
Author SHA1 Message Date
Glavo c9b1cfec5a [release/3.13] 修复 CurseForge 源中内容不显示 Minecraft 26.x 版本的问题 (#6003)
https://github.com/HMCL-dev/HMCL/pull/5994
2026-04-25 20:50:08 +08:00
辞庐 82460ebbcb fix: 下载页面标签显示未进行 shouldDisplayCategory 判断 (#5946) 2026-04-12 21:50:40 +08:00
辞庐 3e8a9bf1c4 清理 DecoratorSkin 冗余 separator (#5947) 2026-04-12 21:50:26 +08:00
辞庐andGlavo a2db925e86 优化 MenuSeparator 颜色 (#5944)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-12 21:44:42 +08:00
辞庐 fe70cc3daf fix: 模组名字过长导致下载页面布局异常 (#5943) 2026-04-12 21:40:05 +08:00
gwdsirandGlavo 5991112cd8 .gitignore 增加 /.gemini/ (#5927)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-12 21:37:19 +08:00
辞庐 084d91c62a 增大自动安装界面加载器名称版本号间距 (#5945) 2026-04-12 21:03:01 +08:00
辞庐 4963f7a5f0 上传微软账户皮肤时校验大小 (#5949) 2026-04-12 20:57:03 +08:00
Glavo f42d5219c2 支持更多 Vulkan 渲染器 (#5940) 2026-04-12 20:41:39 +08:00
Glavo 101a9b5d5e 禁止在 LogWindow 中显示横向滚动条 (#5935) 2026-04-11 00:43:35 +08:00
Glavo 7cd79a4e2a Update versions.txt to 26.1.2 (#5934) 2026-04-10 22:49:40 +08:00
Glavoand3gf8jv4dv 30c3acfb23 增强对 Vulkan 的支持 (#5930)
1. 优化渲染器选项描述;
2. 添加 Vulkan 相关的渲染器选项。

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-04-10 22:19:51 +08:00
Glavo 31f04db555 支持将新格式快照版本号翻译为文言 (#5931) 2026-04-09 00:16:57 +08:00
Glavo 7d41f7bdd3 Add support for Minecraft 26.2-snapshot-1+ to unofficial supported platforms (#5928) 2026-04-08 01:48:50 +08:00
辞庐 8e6ac947ed fix: 安装整合包部分任务未正确归类 (#5922) 2026-04-06 20:38:07 +08:00
辞庐 20e321f502 优化原理图管理&世界管理输入校验 (#5923) 2026-04-06 20:36:49 +08:00
辞庐 00b323e507 优化 Java 下载弹窗 Spinner (#5924) 2026-04-06 20:35:43 +08:00
辞庐 333b593c8a 优化添加认证服务器 http 警告颜色 (#5925) 2026-04-06 20:31:31 +08:00
Glavo 665f6df142 Add IDEA copyright configuration files (#5919) 2026-04-06 13:12:07 +08:00
辞庐 9e16130537 fix: Alex 模型皮肤 3D 预览手臂背后有一列侧面的异常像素 (#5688) 2026-04-05 23:16:40 +08:00
Glavo 5c98d6734d 修复多次打开微软登录弹窗后报错的问题 (#5911) 2026-04-05 23:13:25 +08:00
辞庐 c3801a3445 优化下载页面依赖项展示 (#5872) 2026-04-05 23:10:53 +08:00
辞庐andGlavo 1d137a3a88 优化 ComboBox 展开图标 (#5917)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-05 22:57:37 +08:00
辞庐 2e8b9ae3ca fix: 长按左侧关闭返回按钮可以拖动窗口 (#5915) 2026-04-05 22:57:00 +08:00
Calboot fb750e8717 [Bugfix] 修复导入整合包界面箭头颜色错误的问题 (#5914)
Fixes #5913
2026-04-05 22:15:11 +08:00
辞庐 f9ae0902d8 fix: 安装模组未进行文件名存在检查 (#5879) 2026-04-05 20:33:08 +08:00
GlavoandBurning_TNT 8d69cbc02f Fix #5907: 修复无法为 MC 26.1.1 从官方源安装 NeoForge 的问题 (#5910)
Co-authored-by: Burning_TNT <pangyl08@163.com>
2026-04-04 22:03:54 +08:00
辞庐 90c2372c92 fix: JFXRippler 可能无法覆盖整个控件 (#5609) 2026-04-04 21:44:21 +08:00
Glavo 84c4943de5 搜索 Java 时缓存 Java 信息 (#4716) 2026-04-04 21:41:20 +08:00
3gf8jv4dv 057943b6c0 fix(i18n): clarify terms for specific world detail pages (#5486) 2026-04-04 21:22:41 +08:00
Calboot e943cb0aec [Bugfix] 修复 HMCL 在 1.20.1 neoforge 环境下将部分 forge 模组识别为 MixinExtras 的问题 (#5888)
Fixes #5329
2026-04-04 20:57:09 +08:00
Wulian233 70290e205b 优化 JFXComboBox 样式 添加 hover 背景与手型指针 (#5905) 2026-04-04 20:15:53 +08:00
Glavo a73b8c7172 Add Gemini Code Assist configuration file (#5903) 2026-04-03 23:19:08 +08:00
3gf8jv4dv a5942c8e91 chore: add LWJGL Unsafe Agent to Settings > About page (#5896) 2026-04-03 21:28:42 +08:00
Glavo dc6a97520b fix: ensure main class is not null for game instance (#5902) 2026-04-03 21:27:54 +08:00
8be3f04cc8 feat: 拖拽版本 json 安装实例 (#5730)
resolves #5459

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-03 21:23:14 +08:00
Calboot 8d85b1537d [Bugfix] 修复 HMCL 下载游戏内容时出现空游戏版本条目的问题 (#5899)
Fixes #5894
2026-04-03 20:58:24 +08:00
3gf8jv4dv b16f2a6ce6 chore: update HelloNBT's link on Settings > About page (#5895) 2026-04-03 20:45:39 +08:00
Glavo e19f175e29 优化 Java 下载 (#5859)
1. 清理代码;
2. 先将文件下载至临时文件夹,下载完毕后再移动至目标目录。
2026-04-03 20:44:10 +08:00
63 changed files with 2078 additions and 787 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
code_review:
pull_request_opened:
code_review: false
+5 -1
View File
@@ -64,4 +64,8 @@ language-subtag-registry
# test
/hmcl.json
/.hmcl.json
/.hmcl/
/.hmcl/
# AI
/.gemini/
!/.gemini/config.yaml
+6
View File
@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Hello Minecraft! Launcher&#10;Copyright (C) &amp;#36;today.year huangyuhui &lt;huanghongxun2008@126.com&gt; and contributors&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10;(at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;." />
<option name="myName" value="HMCL" />
</copyright>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="CopyrightManager">
<settings default="HMCL">
<LanguageOptions name="JAVA">
<option name="fileTypeOverride" value="3" />
<option name="addBlankAfter" value="false" />
</LanguageOptions>
</settings>
</component>
@@ -231,10 +231,11 @@ public class JFXRippler extends StackPane {
*
* @return the ripple radius size
*/
protected double computeRippleRadius() {
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
protected double computeRippleRadius(double centerX, double centerY) {
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
}
protected void setOverLayBounds(Rectangle overlay) {
@@ -473,7 +474,7 @@ public class JFXRippler extends StackPane {
super(centerX,
centerY,
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
computeRippleRadius() : getRipplerRadius(), null);
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
@@ -518,7 +519,7 @@ public class JFXRippler extends StackPane {
new KeyValue(opacityProperty(),
1,
RIPPLE_INTERPOLATOR)
), new KeyFrame(Duration.millis(900), inKeyValues));
), new KeyFrame(Duration.millis(600), inKeyValues));
setScaleX(0);
setScaleY(0);
@@ -430,6 +430,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setGraphicsBackend(vs.getGraphicsBackend())
.setRenderer(vs.getRenderer())
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
.setUseNativeGLFW(vs.isUseNativeGLFW())
@@ -173,27 +173,27 @@ public final class LauncherHelper {
}
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|| renderer.mesaDriverName() == null)
return null;
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
})
@@ -232,6 +232,7 @@ public final class LauncherHelper {
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
@@ -51,7 +51,10 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -167,7 +170,7 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static boolean isExternalGameNameConflicts(String name) {
@@ -211,16 +214,16 @@ public final class ModpackHelper {
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
@@ -228,7 +231,7 @@ public final class ModpackHelper {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
default:
throw new UnsupportedModpackException();
}
@@ -163,6 +163,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
@Override
public void close() {
if (!future.isDone())
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
stop();
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
@@ -99,7 +99,7 @@ public final class HMCLJavaRepository implements JavaRepository {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
private static void getAllJava(List<Path> list, Platform platform, Path platformRoot) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
for (Path file : stream) {
try {
@@ -117,8 +117,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
list.add(executable);
}
}
} catch (Throwable e) {
@@ -131,28 +130,23 @@ public final class HMCLJavaRepository implements JavaRepository {
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
public Collection<Path> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
ArrayList<Path> list = new ArrayList<>();
getAllJava(list, platform, platformRoot);
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
@@ -165,16 +159,16 @@ public final class HMCLJavaRepository implements JavaRepository {
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfoUtils.fromExecutable(executable, false);
info = JavaInfoUtils.fromExecutable(executable);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
info = new JavaInfo(platform, result.download().version().name(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.component());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
result.remoteFiles().getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -27,7 +28,6 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
@@ -39,35 +39,10 @@ public final class JavaInfoUtils {
private JavaInfoUtils() {
}
private static Path tryFindReleaseFile(Path executable) {
Path parent = executable.getParent();
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
&& Files.isRegularFile(releaseFile)) {
return releaseFile;
}
}
}
return null;
}
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
public static @NotNull JavaInfo fromExecutable(Path executable) throws IOException {
assert executable.isAbsolute();
Path releaseFile;
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
try {
return JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
Path thisPath = JarUtils.thisJarPath();
if (thisPath == null) {
throw new IOException("Failed to find current HMCL location");
}
@@ -94,7 +69,6 @@ public final class JavaInfoUtils {
? architecture
: Architecture.SYSTEM_ARCH);
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
} catch (IOException e) {
throw e;
@@ -103,14 +77,9 @@ public final class JavaInfoUtils {
}
}
private static final class Result {
@SerializedName("os.name")
public String osName;
@SerializedName("os.arch")
public String osArch;
@SerializedName("java.version")
public String javaVersion;
@SerializedName("java.vendor")
public String javaVendor;
@JsonSerializable
private record Result(@SerializedName("os.name") String osName, @SerializedName("os.arch") String osArch,
@SerializedName("java.version") String javaVersion,
@SerializedName("java.vendor") String javaVendor) {
}
}
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
@@ -30,7 +32,9 @@ 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.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
@@ -40,9 +44,14 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -55,6 +64,16 @@ public final class JavaManager {
private JavaManager() {
}
private static final String[] KNOWN_VENDOR_DIRECTORIES = {
"Java",
"BellSoft",
"AdoptOpenJDK",
"Zulu",
"Microsoft",
"Eclipse Foundation",
"Semeru"
};
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
@@ -160,12 +179,12 @@ public final class JavaManager {
return javaRuntime;
}
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
JavaInfo info = JavaInfoUtils.fromExecutable(executable);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
Task.supplyAsync(() -> searchPotentialJavaExecutables(false)).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
@@ -326,7 +345,7 @@ public final class JavaManager {
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables(true);
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
@@ -334,56 +353,56 @@ public final class JavaManager {
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables(boolean useCache) {
Searcher searcher = new Searcher(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("javaCache.json"));
if (useCache)
searcher.loadCache();
searcher.searchAllJavaInRepository(Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
searcher.searchAllJavaInRepository(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);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
}
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
searcher.queryJavaInRegistryKey(WinReg.HKEY.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)");
}
searcher.searchJavaInProgramFiles("ProgramFiles", "C:\\Program Files");
searcher.searchJavaInProgramFiles("ProgramFiles(x86)", "C:\\Program Files (x86)");
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!
searcher.searchAllJavaInDirectory(Path.of("/usr/java")); // Oracle RPMs
searcher.searchAllJavaInDirectory(Path.of("/usr/lib/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib32/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib64/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
break;
case MACOS:
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
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"));
searcher.searchJavaInMacJavaVirtualMachines(Path.of("/Library/Java/JavaVirtualMachines"));
searcher.searchJavaInMacJavaVirtualMachines(Path.of(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
searcher.tryAddJavaExecutable(Path.of("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
searcher.tryAddJavaExecutable(Path.of("/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@*")) {
searcher.tryAddJavaExecutable(Path.of("/opt/homebrew/opt/java/bin/java"));
searcher.searchAllJavaInDirectory(Path.of("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Path.of("/opt/homebrew/Cellar"), "openjdk@*")) {
for (Path dir : dirs) {
searchAllJavaInDirectory(javaRuntimes, dir);
searcher.searchAllJavaInDirectory(dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
@@ -397,16 +416,16 @@ public final class JavaManager {
// 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));
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
.ifPresent(it -> searcher.searchAllOfficialJava(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);
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
@@ -420,7 +439,7 @@ public final class JavaManager {
}
try {
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
searcher.tryAddJavaExecutable(Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -430,16 +449,17 @@ public final class JavaManager {
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
searcher.tryAddJavaHome(Path.of(path));
} catch (InvalidPathException ignored) {
}
}
}
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
searcher.tryAddJavaExecutable(Path.of(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
@@ -447,233 +467,404 @@ public final class JavaManager {
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !searcher.javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
javaRuntimes.put(currentJava.getBinary(), currentJava);
searcher.addResult(currentJava.getBinary(), currentJava);
}
LOG.trace(javaRuntimes.values().stream().sorted()
searcher.saveCache();
LOG.trace(searcher.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;
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
return searcher.javaRuntimes;
}
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
private static final class Searcher {
private final Path cacheFile;
final Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
private final LinkedHashMap<Path, JavaInfoCache> caches = new LinkedHashMap<>();
private final Set<Path> failed = new HashSet<>();
private boolean needRefreshCache = false;
Searcher(Path cacheFile) {
this.cacheFile = cacheFile;
}
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
private static final Pattern CACHE_VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(?:\\.(?<minor>\\d+))?");
private static final int CACHE_MAJOR_VERSION = 0;
private static final int CACHE_MINOR_VERSION = 0;
private record JavaInfoCache(String key, JavaInfo info) {
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
void loadCache() {
if (Files.notExists(cacheFile))
return;
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
LOG.warning("Failed to read release file " + releaseFile, e);
JsonObject jsonFile = JsonUtils.fromJsonFile(cacheFile, JsonObject.class);
JsonElement fileVersion = jsonFile.get("version");
Matcher matcher;
if (fileVersion instanceof JsonPrimitive version
&& (matcher = CACHE_VERSION_PATTERN.matcher(version.getAsString())).matches()) {
int major = Integer.parseInt(matcher.group("major"));
String minorString = matcher.group("minor");
int minor = minorString != null ? Integer.parseInt(minorString) : 0;
if (major != CACHE_MAJOR_VERSION || minor < CACHE_MINOR_VERSION)
throw new IOException("Unsupported cache file, version: %s".formatted(version.getAsString()));
} else
throw new IOException("Invalid version JSON: " + fileVersion);
JsonArray cachesArray = jsonFile.getAsJsonArray("caches");
for (JsonElement element : cachesArray) {
try {
var obj = (JsonObject) element;
Path realPath = Path.of(obj.getAsJsonPrimitive("path").getAsString()).toRealPath();
String key = obj.getAsJsonPrimitive("key").getAsString();
OperatingSystem osName = OperatingSystem.parseOSName(obj.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(obj.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = obj.getAsJsonPrimitive("java.version").getAsString();
JavaInfo.Builder infoBuilder = JavaInfo.newBuilder(Platform.getPlatform(osName, osArch), javaVersion);
if (obj.get("java.vendor") instanceof JsonPrimitive vendor)
infoBuilder.setVendor(vendor.getAsString());
caches.put(realPath, new JavaInfoCache(key, infoBuilder.build()));
} catch (Exception e) {
LOG.warning("Invalid cache: " + element);
needRefreshCache = true;
}
}
} catch (Exception ex) {
LOG.warning("Failed to load cache file: " + cacheFile);
needRefreshCache = true;
}
}
if (info == null) {
void saveCache() {
if (!needRefreshCache)
return;
needRefreshCache = false;
try {
info = JavaInfoUtils.fromExecutable(executable, false);
FileUtils.saveSafely(cacheFile, output -> {
try (var writer = new JsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
writer.beginObject();
writer.name("version").value("%d.%d".formatted(CACHE_MAJOR_VERSION, CACHE_MINOR_VERSION));
writer.name("caches");
writer.beginArray();
for (Map.Entry<Path, JavaInfoCache> entry : caches.entrySet()) {
Path path = entry.getKey();
JavaInfoCache cache = entry.getValue();
JavaInfo info = cache.info();
writer.beginObject();
writer.name("path").value(path.toString());
writer.name("key").value(cache.key());
writer.name("os.name").value(info.getPlatform().os().getCheckedName());
writer.name("os.arch").value(info.getPlatform().arch().getCheckedName());
writer.name("java.version").value(info.getVersion());
if (info.getVendor() != null)
writer.name("java.vendor").value(info.getVendor());
writer.endObject();
}
writer.endArray();
writer.endObject();
}
});
} catch (Exception e) {
LOG.warning("Failed to save cache file: " + cacheFile);
}
}
private static @Nullable String createCacheKey(Path realPath) {
Path binDir = realPath.getParent();
if (binDir == null || !FileUtils.getName(binDir).equals("bin"))
return null;
if (Files.isRegularFile(realPath.resolveSibling("ikvm.properties")))
return null;
Path javaHome = binDir.getParent();
if (javaHome == null)
return null;
Path libDir = javaHome.resolve("lib");
if (!Files.isDirectory(libDir))
return null;
BasicFileAttributes launcherAttributes;
String releaseHash = null;
BasicFileAttributes coreLibsAttributes = null;
try {
launcherAttributes = Files.readAttributes(realPath, BasicFileAttributes.class);
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
releaseHash = DigestUtils.digestToString("SHA-1", releaseFile);
} else {
Path coreLibsFile = libDir.resolve("rt.jar");
if (!Files.isRegularFile(coreLibsFile)) {
coreLibsFile = javaHome.resolve("jre/lib/rt.jar");
if (!Files.isRegularFile(coreLibsFile))
return null;
coreLibsAttributes = Files.readAttributes(coreLibsFile, BasicFileAttributes.class);
}
}
} catch (Exception e) {
LOG.warning("Failed to create cache key for " + realPath, e);
return null;
}
StringJoiner joiner = new StringJoiner(",");
joiner.add("sz:" + launcherAttributes.size());
joiner.add("lm:" + launcherAttributes.lastModifiedTime().toMillis());
if (releaseHash != null)
joiner.add(releaseHash);
if (coreLibsAttributes != null) {
joiner.add("rsz:" + coreLibsAttributes.size());
joiner.add("rlm:" + coreLibsAttributes.lastModifiedTime().toMillis());
}
return joiner.toString();
}
void addResult(Path realPath, JavaRuntime javaRuntime) {
javaRuntimes.put(realPath, javaRuntime);
}
void tryAddJavaHome(Path javaHome) {
tryAddJavaExecutable(getExecutable(javaHome));
}
void tryAddJavaExecutable(Path executable) {
tryAddJavaExecutable(executable, false);
}
void tryAddJavaExecutable(Path executable, boolean isManaged) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable)
|| failed.contains(executable)
|| ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
String cacheKey = createCacheKey(executable);
if (cacheKey != null) {
JavaInfoCache cache = caches.get(executable);
if (cache != null) {
if (isCompatible(cache.info().getPlatform()) && cacheKey.equals(cache.key())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, cache.info(), isManaged));
return;
} else {
caches.remove(executable);
needRefreshCache = true;
}
}
} else if (caches.remove(executable) != null) {
needRefreshCache = true;
}
JavaInfo info;
try {
info = JavaInfoUtils.fromExecutable(executable);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
failed.add(executable);
return;
}
if (cacheKey != null) {
caches.put(executable, new JavaInfoCache(cacheKey, info));
needRefreshCache = true;
}
javaRuntimes.put(executable, JavaRuntime.of(executable, info, isManaged));
}
void tryAddJavaInComponentDir(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.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(macPath);
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
tryAddJavaHome(dir);
}
void searchAllJavaInRepository(Platform platform) {
for (Path java : REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
if (platform.os() == OperatingSystem.MACOS) {
// In the past, we used 'osx' as the checked name for macOS
Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName());
searchAllJavaInDirectory(platformRoot);
}
}
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
void searchAllOfficialJava(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
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfoUtils.fromExecutable(executable, true);
} 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());
void searchAllOfficialJava(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(platform, component, verify);
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
void searchAllJavaInDirectory(Path directory) {
if (!Files.isDirectory(directory)) {
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);
}
for (JavaRuntime java : LOCAL_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);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_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);
void searchJavaInProgramFiles(String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Path.of(programFiles);
} catch (InvalidPathException ignored) {
return;
}
} 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);
for (String vendor : KNOWN_VENDOR_DIRECTORIES) {
searchAllJavaInDirectory(path.resolve(vendor));
}
} 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));
}
}
private static void searchJavaInMacJavaVirtualMachines(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.resolve("Contents/Home"));
void searchJavaInMacJavaVirtualMachines(Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
// ==== Windows Registry Support ====
void queryJavaInRegistryKey(WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
if (reg.queryValue(hkey, java, "JavaHome") instanceof String home) {
try {
tryAddJavaHome(Path.of(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
}
}
@@ -603,6 +603,20 @@ public final class VersionSetting implements Cloneable, Observable {
processPriorityProperty.set(processPriority);
}
private final ObjectProperty<GraphicsAPI> graphicsBackend = new SimpleObjectProperty<>(this, "graphicsBackend", GraphicsAPI.DEFAULT);
public ObjectProperty<GraphicsAPI> graphicsBackendProperty() {
return graphicsBackend;
}
public GraphicsAPI getGraphicsBackend() {
return graphicsBackendProperty().get();
}
public void setGraphicsBackend(GraphicsAPI api) {
graphicsBackendProperty().set(api);
}
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
public Renderer getRenderer() {
@@ -820,8 +834,9 @@ public final class VersionSetting implements Cloneable, Observable {
}
obj.addProperty("java", java);
obj.addProperty("graphicsBackend", src.getGraphicsBackend().name());
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
if (src.getRenderer() == Renderer.OpenGL.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
return obj;
@@ -894,16 +909,19 @@ public final class VersionSetting implements Cloneable, Observable {
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.map(Renderer::of).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
}));
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
return Optional.of(GraphicsAPI.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
}));
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
return vs;
}
@@ -105,9 +105,14 @@ public final class Controllers {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
Path modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
FXUtils.applyDragListener(gameListPage, file -> ModpackHelper.isFileModpackByExtension(file) || "json".equalsIgnoreCase(FileUtils.getNameWithoutExtension(file)), files -> {
Path file = files.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file), i18n("install.modpack"));
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
}
});
return gameListPage;
});
@@ -170,6 +170,7 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.getStyleClass().add("no-horizontal-scrollbar");
listView.getProperties().put("no-smooth-scrolling", true);
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
@@ -164,6 +164,9 @@ public class AccountListItem extends RadioButton {
if (skinImg.isError()) {
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
}
if (skinImg.getWidth() != 64 || (skinImg.getHeight() != 32 && skinImg.getHeight() != 64)) {
throw new InvalidSkinException("Invalid skin size");
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
@@ -115,7 +115,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
GridPane.setRowIndex(lblServerName, 1);
lblServerWarning = new Label(i18n("account.injector.http"));
lblServerWarning.setStyle("-fx-text-fill: red;");
lblServerWarning.setStyle("-fx-text-fill: -monet-error;");
GridPane.setColumnIndex(lblServerWarning, 0);
GridPane.setRowIndex(lblServerWarning, 2);
lblServerWarning.managedProperty().bind(lblServerWarning.visibleProperty());
@@ -28,6 +28,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.*;
import javafx.scene.layout.StackPane;
@@ -49,6 +50,8 @@ public class LineSelectButton<T> extends LineButton {
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
private JFXPopup popup;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private ObservableList<Node> popupItems; // keep a reference
public LineSelectButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
@@ -87,7 +90,7 @@ public class LineSelectButton<T> extends LineButton {
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
Bindings.bindContent(popupMenu.getContent(), popupItems = MappedObservableList.create(itemsProperty(), item -> {
VBox vbox = new VBox();
var itemTitleLabel = new Label();
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.theme.Themes;
public class MenuSeparator extends StackPane {
@@ -28,7 +28,7 @@ public class MenuSeparator extends StackPane {
Rectangle rect = new Rectangle();
rect.widthProperty().bind(widthProperty().add(-14));
rect.setHeight(1);
rect.setFill(Color.GRAY);
rect.fillProperty().bind(Themes.colorSchemeProperty().getOutlineVariant());
maxHeightProperty().set(10);
setPadding(new Insets(3));
getChildren().setAll(rect);
@@ -147,6 +147,10 @@ public class TwoLineListItem extends VBox {
subtitleProperty().set(subtitle);
}
public HBox getFirstLine() {
return firstLine;
}
public Label getTitleLabel() {
return lblTitle;
}
@@ -168,6 +172,7 @@ public class TwoLineListItem extends VBox {
Bindings.bindContent(tagsBox.getChildren(), tags);
var scrollPane = new ScrollPane(tagsBox);
scrollPane.setMinSize(0, 0);
HBox.setHgrow(scrollPane, Priority.ALWAYS);
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
@@ -274,6 +274,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canBack) {
JFXButton backNavButton = new JFXButton();
skinnable.forbidDraggingWindow(backNavButton);
backNavButton.setFocusTraversable(false);
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
backNavButton.getStyleClass().add("jfx-decorator-button");
@@ -285,6 +286,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canClose) {
JFXButton closeNavButton = new JFXButton();
skinnable.forbidDraggingWindow(closeNavButton);
closeNavButton.setFocusTraversable(false);
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
closeNavButton.getStyleClass().add("jfx-decorator-button");
@@ -349,13 +351,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
refreshNavButton.getStyleClass().add("jfx-decorator-button");
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
skinnable.forbidDraggingWindow(refreshNavButton);
Rectangle separator = new Rectangle();
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
separator.heightProperty().bind(navBar.heightProperty());
separator.setFill(Color.GRAY);
navRight.getChildren().setAll(refreshNavButton, separator);
navRight.getChildren().setAll(refreshNavButton);
navBar.setRight(navRight);
}
}
@@ -54,13 +54,18 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
@@ -134,6 +139,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Set<String> existingFiles;
try (var list = Files.list(runDirectory.resolve(subdirectoryName))) {
existingFiles = list.map(Path::getFileName)
.map(Path::toString)
.collect(Collectors.toSet());
} catch (IOException e) {
LOG.warning("Failed to list files in " + runDirectory.resolve(subdirectoryName), e);
existingFiles = Set.of();
}
Set<String> finalExistingFiles = existingFiles;
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
@@ -153,7 +171,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
handler.resolve();
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}
@@ -97,8 +97,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
graphic.setMouseTransparent(true);
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
SVGPath arrow = new SVGPath();
arrow.setContent(SVG.ARROW_FORWARD.getPath());
SVGPath arrow = SVG.ARROW_FORWARD.createIcon();
BorderPane.setAlignment(arrow, Pos.CENTER);
graphic.setRight(arrow);
@@ -47,6 +47,7 @@ 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.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Result;
@@ -233,7 +234,6 @@ public final class JavaDownloadDialog extends StackPane {
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()));
@@ -300,6 +300,11 @@ public final class JavaDownloadDialog extends StackPane {
remoteVersionBox.getSelectionModel().selectFirst();
});
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("small-spinner-pane");
spinnerPane.setContent(downloadButton);
downloadButtonPane.getChildren().setAll(spinnerPane);
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
remoteVersionBox.setItems(null);
packageTypeBox.getItems().clear();
@@ -308,11 +313,11 @@ public final class JavaDownloadDialog extends StackPane {
warningLabel.setText(null);
if (list == null || (list.versions != null && list.versions.isEmpty()))
downloadButtonPane.getChildren().setAll(downloadButton);
spinnerPane.hideSpinner();
else if (list.status == DiscoJavaVersionList.Status.LOADING)
downloadButtonPane.getChildren().setAll(new JFXSpinner());
spinnerPane.showSpinner();
else {
downloadButtonPane.getChildren().setAll(downloadButton);
spinnerPane.hideSpinner();
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
packageTypeBox.getItems().setAll(list.versions.keySet());
@@ -54,6 +54,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -98,7 +99,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage,
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file) || "json".equalsIgnoreCase(FileUtils.getExtension(file)),
modpacks -> {
Path file = modpacks.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
@@ -113,6 +114,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
}
});
@@ -33,7 +33,7 @@ public class SkinCube extends MeshView {
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
float startX, float startY, boolean isSlim) {
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = half_width,
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
return new float[]{
top_x, top_y, // T0 ---
@@ -25,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GraphicsAPI;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.Renderer;
import org.jackhuang.hmcl.setting.Profile;
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.FileSystems;
@@ -69,6 +71,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final LineSelectButton<GraphicsAPI> graphicsBackendPane;
private final LineSelectButton<Renderer> rendererPane;
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
@@ -79,6 +82,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
));
@Nullable GameVersionNumber gameVersion = versionId != null
? GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId))
: null;
this.getStyleClass().add("gray-background");
ScrollPane scrollPane = new ScrollPane();
@@ -185,6 +192,25 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
nativesDirItem.getChildren().add(nativesDirHint);
graphicsBackendPane = new LineSelectButton<>();
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
graphicsBackendPane.setConverter(backend -> i18n("settings.advanced.graphics_backend." + backend.name().toLowerCase(Locale.ROOT)));
graphicsBackendPane.setDescriptionConverter(backend -> switch (backend) {
case DEFAULT -> i18n("settings.advanced.graphics_backend.default.desc");
case OPENGL -> i18n("settings.advanced.graphics_backend.opengl.desc");
case VULKAN -> {
if (gameVersion == null)
yield i18n("settings.advanced.graphics_backend.vulkan.desc.global");
else if (gameVersion.compareTo("26.2-snapshot-2") < 0)
yield i18n("settings.advanced.graphics_backend.vulkan.desc.unsupported");
else
yield i18n("settings.advanced.graphics_backend.vulkan.desc");
}
default -> null;
});
graphicsBackendPane.setValue(GraphicsAPI.DEFAULT);
graphicsBackendPane.setItems(GraphicsAPI.values());
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
@@ -192,7 +218,23 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
rendererPane.setItems(Renderer.values());
rendererPane.setValue(Renderer.DEFAULT);
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
if (backend == null) { // unbind
return;
}
rendererPane.setItems(Renderer.getSupported(backend));
if (backend == GraphicsAPI.DEFAULT) {
rendererPane.setDisable(true);
rendererPane.setValue(Renderer.DEFAULT);
} else {
rendererPane.setDisable(false);
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
rendererPane.setValue(Renderer.DEFAULT);
}
});
noJVMArgsPane = new LineToggleButton();
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
@@ -219,7 +261,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
workaroundPane.getContent().setAll(
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
nativesDirSublist, graphicsBackendPane, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
noJVMCheckPane, noNativesPatchPane
);
@@ -252,6 +294,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
graphicsBackendPane.valueProperty().bindBidirectional(versionSetting.graphicsBackendProperty());
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
@@ -294,6 +337,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
graphicsBackendPane.valueProperty().unbindBidirectional(versionSetting.graphicsBackendProperty());
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
@@ -225,8 +225,10 @@ public class DownloadPage extends Control implements DecoratorPage {
content.setSubtitle(getSkinnable().addon.getDescription());
content.getSubtitleLabel().setWrapText(true);
getSkinnable().addon.getCategories().stream()
.filter(category -> getSkinnable().page.shouldDisplayCategory(category))
.map(category -> getSkinnable().page.getLocalizedCategory(category))
.forEach(content::addTag);
content.getFirstLine().setMinWidth(0);
descriptionPane.getChildren().add(content);
if (getSkinnable().mod != null) {
@@ -273,7 +275,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
String gameVersion = repository.getGameVersion(game).orElse(null);
if (gameVersion != null) {
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
List<RemoteMod.Version> modVersions = control.versions.get(gameVersion);
if (modVersions != null && !modVersions.isEmpty()) {
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
@@ -351,7 +353,7 @@ public class DownloadPage extends Control implements DecoratorPage {
}
}
private static final class DependencyModItem extends StackPane {
private static final class DependencyModItem extends LineButton {
public static final EnumMap<RemoteMod.DependencyType, String> I18N_KEY = new EnumMap<>(Lang.mapOf(
Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"),
Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"),
@@ -367,16 +369,16 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.setPadding(new Insets(0, 8, 0, 8));
pane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem content = new TwoLineListItem();
pane.setMouseTransparent(true);
HBox.setHgrow(content, Priority.ALWAYS);
var imageView = new ImageContainer(40);
pane.getChildren().setAll(imageView, content);
RipplerContainer container = new RipplerContainer(pane);
FXUtils.onClicked(container, () -> {
FXUtils.setLimitHeight(this, 60);
setOnAction((e) -> {
fireEvent(new DialogCloseEvent());
Controllers.navigate(new DownloadPage(page, addon, version, callback));
});
getChildren().setAll(container);
setNode(IDX_LEADING, pane);
if (addon != RemoteMod.BROKEN) {
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
@@ -493,6 +495,7 @@ public class DownloadPage extends Control implements DecoratorPage {
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setContent(dependenciesList);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
@@ -23,7 +23,10 @@ import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
@@ -157,20 +160,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
return;
Path parent = currentDirectory.path;
Controllers.dialog(new InputDialogPane(
Controllers.prompt(
i18n("schematics.create_directory.prompt"),
"",
(result, handler) -> {
if (StringUtils.isBlank(result)) {
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
return;
}
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
return;
}
Path targetDir = parent.resolve(result);
if (Files.exists(targetDir)) {
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
@@ -185,7 +177,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
LOG.warning("Failed to create directory: " + targetDir, e);
handler.reject(i18n("schematics.create_directory.failed", targetDir));
}
}));
}, "", new RequiredValidator(), new Validator(i18n("schematics.create_directory.failed.invalid_name"), FileUtils::isNameValid));
}
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
@@ -22,8 +22,11 @@ import javafx.application.Platform;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*;
@@ -146,7 +149,8 @@ public final class Versions {
} else {
handler.reject(i18n("version.manage.rename.fail"));
}
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
}, version,
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
}
@@ -158,6 +162,43 @@ public final class Versions {
FXUtils.openFolder(profile.getRepository().getRunDirectory(version));
}
public static void installFromJson(Profile profile, Path file) {
Version version;
try {
version = profile.getRepository().readVersionJson(file);
} catch (Exception e) {
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> {
handler.resolve();
DefaultDependencyManager dependencyManager = profile.getDependency();
HMCLGameRepository repository = profile.getRepository();
Version newVersion = version.setId(result).setJar(result);
Controllers.taskDialog(
Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion),
Task.allOf(
new GameAssetDownloadTask(dependencyManager, newVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true),
new GameLibrariesTask(dependencyManager, newVersion, true)
).withRunAsync(() -> {
// ignore failure
}))
.thenComposeAsync(repository.saveAsync(newVersion))
.thenRunAsync(repository::refreshVersions)
.whenComplete(Schedulers.javafx(), (exception) -> {
if (exception == null) {
profile.setSelectedVersion(result);
} else {
Controllers.dialog(
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
}
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName)));
}
public static void duplicateVersion(Profile profile, String version) {
Controllers.prompt(
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
@@ -23,8 +23,9 @@ import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -89,20 +90,9 @@ public final class WorldManageUIUtils {
public static void copyWorld(World world, Runnable runnable) {
Path worldPath = world.getFile();
Controllers.dialog(new InputDialogPane(
Controllers.prompt(
i18n("world.duplicate.prompt"),
"",
(result, handler) -> {
if (StringUtils.isBlank(result)) {
handler.reject(i18n("world.duplicate.failed.empty_name"));
return;
}
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
handler.reject(i18n("world.duplicate.failed.invalid_name"));
return;
}
Path targetDir = worldPath.resolveSibling(result);
if (Files.exists(targetDir)) {
handler.reject(i18n("world.duplicate.failed.already_exists"));
@@ -125,7 +115,7 @@ public final class WorldManageUIUtils {
}
})
.start();
}));
}, "", new RequiredValidator(), new Validator(i18n("world.duplicate.failed.invalid_name"), FileUtils::isNameValid));
}
public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException {
@@ -189,15 +189,16 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
/// @see <a href="https://github.com/HMCL-dev/mesa-loader-windows">Java Mesa Loader for Windows</a>
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime java, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
return getNatives(java.getPlatform()).get("mesa-loader");
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
if (renderer == Renderer.OpenGL.LLVMPIPE)
return getNatives(java.getPlatform()).get("software-renderer-loader");
else
return null;
} else {
@@ -96,6 +96,10 @@ public class Translator_lzh extends Translator {
case GA -> {
// do nothing
}
case SNAPSHOT -> {
builder.append("之快照");
appendDigitByDigit(builder, release.getEaVersion().toString());
}
case PRE_RELEASE -> {
builder.append("之預");
appendDigitByDigit(builder, release.getEaVersion().toString());
@@ -47,7 +47,7 @@
{
"title" : "HelloNBT",
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/Glavo/HelloNBT"
"externalLink" : "https://github.com/HMCL-dev/HelloNBT"
},
{
"title" : "minecraft-jfx-skin",
@@ -88,5 +88,10 @@
"title": "MonetFX",
"subtitle": "Copyright © 2025 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink": "https://github.com/Glavo/MonetFX"
},
{
"title": "LWJGL Unsafe Agent",
"subtitle": "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink": "https://github.com/HMCL-dev/lwjgl-unsafe-agent"
}
]
+11 -1
View File
@@ -414,7 +414,7 @@
}
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
-fx-pref-width: 80px;
-fx-pref-width: 100px;
}
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
@@ -1482,6 +1482,11 @@
-fx-max-width: 1000000000;
}
.jfx-combo-box .arrow {
-fx-background-color: -monet-on-surface-variant;
-fx-shape: "M12 15 7 10H17L12 15Z";
}
.jfx-combo-box .text {
-fx-fill: -monet-on-surface;
}
@@ -1497,6 +1502,7 @@
.combo-box-popup .list-view .jfx-list-cell {
-fx-background-color: -monet-surface-container;
-fx-text-fill: -monet-on-surface;
-fx-cursor: hand;
-fx-background-insets: 0.0;
}
@@ -1506,6 +1512,10 @@
-fx-text-fill: -monet-on-secondary-container;
}
.combo-box-popup .list-view .list-cell:hover > .jfx-rippler > StackPane {
-fx-background-color: -monet-secondary-container-transparent-50;
}
.jfx-combo-box-warning {
-jfx-focus-color: #D34336;
-jfx-unfocus-color: #D34336;
@@ -762,6 +762,7 @@ install.new_game.already_exists=This instance name already exists. Please use an
install.new_game.current_game_version=Current Instance Version
install.new_game.installation=Instance Installation
install.new_game.malformed=Invalid name.
install.new_game.malformed_json=Invalid Minecraft instance JSON file.
install.select=Choose operation
install.success=Successfully installed.
@@ -1231,7 +1232,7 @@ world.load.fail=Failed to load world
world.locked=In use
world.locked.failed=The world is currently in use. Please close the game and try again.
world.manage=Worlds
world.manage.button=World Management
world.manage.button=Manage World
world.manage.title=World - %s
world.name=World Name
world.name.enter=Enter the world name
@@ -1336,6 +1337,15 @@ settings.advanced.dont_patch_natives=Do not attempt to automatically replace nat
settings.advanced.environment_variables=Environment Variables
settings.advanced.game_dir.default=Default (".minecraft/")
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
settings.advanced.graphics_backend=Graphics API
settings.advanced.graphics_backend.default=Default
settings.advanced.graphics_backend.default.desc=Follow the game's settings
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=Use OpenGL for rendering
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=Use Vulkan for rendering
settings.advanced.graphics_backend.vulkan.desc.global=Use Vulkan for rendering (Since Minecraft 26.2)
settings.advanced.graphics_backend.vulkan.desc.unsupported=Use Vulkan for rendering (not supported for the current game version)
settings.advanced.java_permanent_generation_space=PermGen Space
settings.advanced.java_permanent_generation_space.prompt=in MiB
settings.advanced.jvm=JVM Options
@@ -1378,13 +1388,47 @@ settings.advanced.post_exit_command=Post-exit Command
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
settings.advanced.renderer=Renderer
settings.advanced.renderer.default=Default
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
settings.advanced.renderer.default.desc=Use System Default
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=Vulkan renderer based on DirectX 12 (Experimental)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=Official Vulkan driver for NVIDIA GPUs
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=Open-source Vulkan driver for NVIDIA GPUs
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=Official Vulkan driver for AMD GPUs
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=Open-source Vulkan driver for AMD GPUs
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Official Vulkan driver for Intel GPUs
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Open-source Vulkan driver for Intel GPUs
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Open-source Vulkan driver for older Intel GPUs
settings.advanced.renderer.qualcomm=Qualcomm
settings.advanced.renderer.qualcomm.desc=Official Vulkan driver for Qualcomm Adreno GPUs
settings.advanced.renderer.turnip=Qualcomm (Turnip)
settings.advanced.renderer.turnip.desc=Open-source Vulkan driver for Qualcomm Adreno GPUs
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=Vulkan renderer based on Metal
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=Vulkan renderer based on Metal 4 (Experimental)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=Open-source Vulkan driver for PowerVR GPUs (Experimental)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Open-source Vulkan driver for Arm Mali GPUs (Experimental)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=Open-source Vulkan driver for Raspberry Pi 4/5 GPUs (Experimental)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
settings.advanced.renderer.llvmpipe.desc=Software OpenGL renderer
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
settings.advanced.server_ip=Server Address
settings.advanced.server_ip.prompt=Automatically join after launching the game
settings.advanced.unsupported_system_options=Settings not applicable to the current system
@@ -1218,7 +1218,6 @@ world.load.fail=فشل تحميل العالم
world.locked=قيد الاستخدام
world.locked.failed=العالم قيد الاستخدام حالياً. يُرجى إغلاق اللعبة والمحاولة مجدداً.
world.manage=العوالم
world.manage.button=إدارة العوالم
world.manage.title=العالم - %s
world.name=اسم العالم
world.name.enter=أدخل اسم العالم
@@ -1365,13 +1364,9 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
settings.advanced.renderer=المُصيِّر
settings.advanced.renderer.default=افتراضي
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيفان)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=برمجي (أداء ضعيف، توافق أفضل)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
settings.advanced.server_ip=عنوان الخادم
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
@@ -1138,7 +1138,6 @@ world.info.random_seed=Semilla
world.locked=En uso
world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo.
world.manage=Mundos
world.manage.button=Administrar
world.manage.title=Mundo - %s
world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
@@ -1269,13 +1268,9 @@ settings.advanced.post_exit_command=Comando post-salida
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
settings.advanced.renderer=Renderizador
settings.advanced.renderer.default=Por defecto
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
settings.advanced.server_ip=Dirección del servidor
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
@@ -792,13 +792,9 @@ settings.advanced.post_exit_command=終了後のコマンド
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
settings.advanced.renderer=レンダラー
settings.advanced.renderer.default=デフォルト
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (低パフォーマンス、低互換性)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ソフトウェア (低速だが、互換性は最高)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低互換性)
settings.advanced.server_ip=サーバーアドレス
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
@@ -951,8 +951,8 @@ world.info.player.xp_level=經驗之層
world.info.random_seed=種
world.locked=見用
world.manage=司生界
world.manage.button=司生界
world.manage.title=司生界 - %s
world.manage.button=司此生界
world.manage.title=生界 - %s
world.name=生界之名
world.name.enter=書生界之名
world.show_all=全顯
@@ -1085,13 +1085,9 @@ settings.advanced.post_exit_command=令於戲訖
settings.advanced.post_exit_command.prompt=將後戲訖而行
settings.advanced.renderer=繪器
settings.advanced.renderer.default=本
settings.advanced.renderer.default.desc=開圖庫
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (效與所適皆差,勘誤之用)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪器 (效差,所適至)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
settings.advanced.server_ip=伺服器之址
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
settings.advanced.unsupported_system_options=不逮今之械網所選
@@ -1130,7 +1130,6 @@ world.info.random_seed=Ключ генератора мира
world.locked=В эксплуатации
world.locked.failed=В настоящее время мир находится в эксплуатации. Закройте игру и попробуйте снова.
world.manage=Миры
world.manage.button=Управлять
world.manage.title=Мир - %s
world.name=Название мира
world.name.enter=Введите название мира
@@ -1260,13 +1259,9 @@ settings.advanced.post_exit_command=Команда после выхода
settings.advanced.post_exit_command.prompt=Команды, которые необходимо выполнить после выхода из игры
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Низкая производительность и совместимость)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ПО (Низкая производительность, лучшая совместимость)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Лучшая производительность, низкая совместимость)
settings.advanced.server_ip=Адрес сервера
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
@@ -1078,7 +1078,6 @@ world.info.random_seed=Насіння
world.locked=Використовується
world.locked.failed=Світ наразі використовується. Закрийте гру та спробуйте знову.
world.manage=Світи
world.manage.button=Керування світами
world.manage.title=Світ - %s
world.name=Назва світу
world.name.enter=Введіть назву світу
@@ -1207,13 +1206,9 @@ settings.advanced.post_exit_command=Команда після виходу
settings.advanced.post_exit_command.prompt=Команди для виконання після виходу з гри
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Погана продуктивність та сумісність)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Програмний (Погана продуктивність, найкраща сумісність)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктивність, погана сумісність)
settings.advanced.server_ip=Адреса сервера
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
@@ -566,6 +566,7 @@ install.new_game.already_exists=此實例已經存在,請重新命名
install.new_game.current_game_version=目前遊戲實例
install.new_game.installation=安裝新實例
install.new_game.malformed=名稱無效
install.new_game.malformed_json=無效的 Minecraft 實例 JSON 文件。
install.select=請選取安裝方式
install.success=安裝成功
@@ -1025,8 +1026,8 @@ world.locked=使用中
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
world.game_version=遊戲版本
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.manage.button=管理此世界
world.manage.title=世界 - %s
world.name=世界名稱
world.name.enter=輸入世界名稱
world.show_all=全部顯示
@@ -1126,6 +1127,15 @@ settings.advanced.dont_patch_natives=不嘗試自動取代本機庫
settings.advanced.environment_variables=環境變數
settings.advanced.game_dir.default=預設 (".minecraft/")
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
settings.advanced.graphics_backend=圖形 API
settings.advanced.graphics_backend.default=預設
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (適用於 Minecraft 26.2 及更高版本)
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不適用於當前遊戲版本)
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
settings.advanced.java_permanent_generation_space.prompt=單位 MiB
settings.advanced.jvm=Java 虛擬機設定
@@ -1163,15 +1173,49 @@ settings.advanced.process_priority.high=高
settings.advanced.process_priority.high.desc=優先保證遊戲執行,但可能會導致其他程式卡頓
settings.advanced.post_exit_command=遊戲結束後執行指令
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
settings.advanced.renderer=繪製器
settings.advanced.renderer=繪製器/驅動
settings.advanced.renderer.default=預設
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (效能與相容性較差,用於除錯)
settings.advanced.renderer.default.desc=使用系統預設繪製器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=軟體 Vulkan 繪製器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基於 DirectX 12 的 Vulkan 繪製器 (實驗性)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驅動
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的開源 Vulkan 驅動
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驅動
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=AMD GPU 的開源 Vulkan 驅動
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驅動
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Intel GPU 的開源 Vulkan 驅動
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Intel 舊 GPU 的開源 Vulkan 驅動
settings.advanced.renderer.qualcomm=高通
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驅動
settings.advanced.renderer.turnip=高通 (Turnip)
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的開源 Vulkan 驅動
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=基於 Metal 的 Vulkan 繪製器
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=基於 Metal 4 的 Vulkan 繪製器 (實驗性)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=PowerVR GPU 的開源 Vulkan 驅動 (實驗性)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的開源 Vulkan 驅動 (實驗性)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=樹莓派 4/5 GPU 的開源 Vulkan 驅動 (實驗性)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪製器 (效能較差,相容性最好)
settings.advanced.renderer.llvmpipe.desc=軟體 OpenGL 繪製器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基於 DirectX 12 的 OpenGL 繪製器
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
settings.advanced.renderer.zink.desc=基於 Vulkan 的 OpenGL 繪製器
settings.advanced.server_ip=伺服器位址
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
settings.advanced.unsupported_system_options=不適用於目前系統的選項
@@ -571,6 +571,7 @@ install.new_game.already_exists=此实例已经存在,请换一个名字
install.new_game.current_game_version=当前游戏实例
install.new_game.installation=安装新游戏
install.new_game.malformed=名字不合法
install.new_game.malformed_json=不是有效的 Minecraft 实例 JSON 文件。
install.select=请选择安装方式
install.success=安装成功
@@ -1030,8 +1031,8 @@ world.load.fail=世界加载失败
world.locked=使用中
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.manage.button=管理此世界
world.manage.title=世界 - %s
world.name=世界名称
world.name.enter=输入世界名称
world.show_all=显示全部
@@ -1131,6 +1132,15 @@ settings.advanced.dont_patch_natives=不尝试自动替换本地库
settings.advanced.environment_variables=环境变量
settings.advanced.game_dir.default=默认 (".minecraft/")
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
settings.advanced.graphics_backend=图形 API
settings.advanced.graphics_backend.default=默认
settings.advanced.graphics_backend.default.desc=遵循游戏设置
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (适用于 Minecraft 26.2 及更高版本)
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不适用于当前游戏版本)
settings.advanced.java_permanent_generation_space=内存永久保存区域
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
settings.advanced.jvm=Java 虚拟机设置
@@ -1168,15 +1178,49 @@ settings.advanced.process_priority.high=高
settings.advanced.process_priority.high.desc=优先保证游戏运行,但可能会导致其他程序卡顿
settings.advanced.post_exit_command=游戏结束后执行命令
settings.advanced.post_exit_command.prompt=将在游戏结束后调用
settings.advanced.renderer=渲染器
settings.advanced.renderer=渲染器/驱动
settings.advanced.renderer.default=默认
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (性能与兼容性较差,用于调试)
settings.advanced.renderer.default.desc=使用系统默认渲染器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=软件 Vulkan 渲染器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基于 DirectX 12 的 Vulkan 渲染器 (实验性)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驱动
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的开源 Vulkan 驱动
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驱动
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=AMD GPU 的开源 Vulkan 驱动
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驱动
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Intel GPU 的开源 Vulkan 驱动
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Intel 旧 GPU 的开源 Vulkan 驱动
settings.advanced.renderer.qualcomm=高通
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驱动
settings.advanced.renderer.turnip=高通 (Turnip)
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的开源 Vulkan 驱动
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=基于 Metal 的 Vulkan 渲染器
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=基于 Metal 4 的 Vulkan 渲染器 (实验性)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=PowerVR GPU 的开源 Vulkan 驱动 (实验性)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的开源 Vulkan 驱动 (实验性)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=树莓派 4/5 GPU 的开源 Vulkan 驱动 (实验性)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=软渲染器 (性能较差,兼容性最好)
settings.advanced.renderer.llvmpipe.desc=软件 OpenGL 渲染器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基于 DirectX 12 的 OpenGL 渲染器
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
settings.advanced.renderer.zink.desc=基于 Vulkan 的 OpenGL 渲染器
settings.advanced.server_ip=服务器地址
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
settings.advanced.unsupported_system_options=不适用于当前系统的选项
+205 -103
View File
@@ -781,17 +781,6 @@
}
}
},
"org.lwjgl:lwjgl:3.4.1": {
"name": "org.lwjgl:lwjgl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
"sha1": "6105690714874b995aa7812e4f7732a21109276c",
"size": 1157957
}
}
},
"org.lwjgl:lwjgl:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl:3.4.1:natives-linux-arm64",
"downloads": {
@@ -803,17 +792,6 @@
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.1": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
"sha1": "16b663092854fc6adfe0a941e61cae2bb2e437b6",
"size": 47872
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1:natives-linux-arm64",
"downloads": {
@@ -825,17 +803,6 @@
}
}
},
"org.lwjgl:lwjgl-openal:3.4.1": {
"name": "org.lwjgl:lwjgl-openal:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
"sha1": "8508bd60589de0539aa465b78fcf838efd07ae9a",
"size": 153919
}
}
},
"org.lwjgl:lwjgl-openal:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-openal:3.4.1:natives-linux-arm64",
"downloads": {
@@ -847,17 +814,6 @@
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.1": {
"name": "org.lwjgl:lwjgl-opengl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
"sha1": "830412cab823c029cda6b9729f9d2ed36cf79959",
"size": 937660
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-opengl:3.4.1:natives-linux-arm64",
"downloads": {
@@ -869,17 +825,6 @@
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.1": {
"name": "org.lwjgl:lwjgl-glfw:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
"sha1": "a782b1ddd175c9107bf300fd92920579ea65e2a4",
"size": 151893
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-glfw:3.4.1:natives-linux-arm64",
"downloads": {
@@ -891,17 +836,6 @@
}
}
},
"org.lwjgl:lwjgl-stb:3.4.1": {
"name": "org.lwjgl:lwjgl-stb:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
"sha1": "f077c3dafc31924fbe8acb0b913f08977ba27f8e",
"size": 142927
}
}
},
"org.lwjgl:lwjgl-stb:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-stb:3.4.1:natives-linux-arm64",
"downloads": {
@@ -913,17 +847,6 @@
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.1": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
"sha1": "562697d40dbd2ed0424bf24dcf65d51170f81adf",
"size": 15931
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux-arm64",
"downloads": {
@@ -935,17 +858,6 @@
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.1": {
"name": "org.lwjgl:lwjgl-freetype:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
"sha1": "caa75a37e857efc9b0686e434d708d6420c5b6ea",
"size": 464342
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-freetype:3.4.1:natives-linux-arm64",
"downloads": {
@@ -957,6 +869,39 @@
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-arm64.jar",
"sha1": "bc5c99b2eb7e2109c6db21cfced15a6851f8111b",
"size": 3397476
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-arm64.jar",
"sha1": "e2eaa8f516d43fe11b8253e6243b3d8e1315c9c8",
"size": 1139919
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-arm64.jar",
"sha1": "de40d43c5947c4a4f91376af8f2c00e5396cc109",
"size": 59715
}
}
},
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
"downloads": {
@@ -2689,6 +2634,9 @@
"org.lwjgl:lwjgl-stb:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-freetype:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": null,
"net.java.dev.jna:jna:5.8.0": {
"name": "net.java.dev.jna:jna:5.13.0",
"downloads": {
@@ -4359,6 +4307,83 @@
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
"size": 153051
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-riscv64.jar",
"sha1": "5598b0b86d4b3ca0ae78817719d5814efb9c7a77",
"size": 3147886
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
"size": 148285
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-riscv64.jar",
"sha1": "ea09f614c961b14962e4cc55a05c6767f9440f3f",
"size": 1215586
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1": {
"name": "org.lwjgl:lwjgl-vma:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
"size": 109209
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-riscv64.jar",
"sha1": "9616440ee3cb2cfeacd99a61eefded856b5d7efe",
"size": 57091
}
}
},
"org.lwjgl:lwjgl-vulkan:3.4.1": {
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
"size": 8356700
}
}
},
"com.github.oshi:oshi-core:6.6.5": {
"name": "com.github.oshi:oshi-core:6.8.0",
"downloads": {
@@ -4378,13 +4403,13 @@
},
"windows-x86_64": {
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:x64",
"name": "org.glavo:mesa-loader-windows:26.0.4:x64",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
"sha1": "b2552fcc8c98e95b4559c39f2ff87cdb3aaaa513",
"size": 27971214
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
"sha1": "f8da709c59ef61f531c91434ca0e3b4f39202981",
"size": 49873846
}
}
},
@@ -4402,13 +4427,13 @@
},
"windows-x86": {
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:x86",
"name": "org.glavo:mesa-loader-windows:26.0.4:x86",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
"sha1": "6fd0fd7da88ca6636f735e1e0f6feb7f62f59715",
"size": 23311299
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
"sha1": "ac6afaa8baa7c17468267c09e77e1296ee92d5ed",
"size": 41742113
}
}
}
@@ -4791,13 +4816,13 @@
"com.mojang:text2speech:1.12.4:natives": null,
"com.mojang:text2speech:1.13.9:natives-windows": null,
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:arm64",
"name": "org.glavo:mesa-loader-windows:26.0.4:arm64",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
"sha1": "40ca5de3351bf48b7b9efa5d2598d5b6b4d1ab8e",
"size": 24905270
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
"sha1": "5c761a344700a07eaded51c5cf0cde36ee614706",
"size": 43504189
}
}
}
@@ -6325,6 +6350,83 @@
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
"size": 153051
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-freebsd.jar",
"sha1": "872460f4b007ed3a13a2d311e90bda5370406fc8",
"size": 2953522
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
"size": 148285
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-freebsd.jar",
"sha1": "7a3d0e4a74f022b3196ecd33d9ebffafec7767f4",
"size": 1094646
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1": {
"name": "org.lwjgl:lwjgl-vma:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
"size": 109209
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-freebsd.jar",
"sha1": "85851480bef99188bf9a84b36cbe862cb4a52016",
"size": 62862
}
}
},
"org.lwjgl:lwjgl-vulkan:3.4.1": {
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
"size": 8356700
}
}
},
"net.java.jinput:jinput-platform:2.0.5:natives": null,
"com.mojang:text2speech:1.10.3:natives": null,
"com.mojang:text2speech:1.11.3:natives": null,
@@ -80,37 +80,37 @@ public class OAuth {
}
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
Session session = options.callback.startServer();
try (Session session = options.callback.startServer()) {
String codeVerifier = session.getCodeVerifier();
String state = session.getState();
String codeChallenge = generateCodeChallenge(codeVerifier);
String codeVerifier = session.getCodeVerifier();
String state = session.getState();
String codeChallenge = generateCodeChallenge(codeVerifier);
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
mapOf(pair("client_id", options.callback.getClientId()),
pair("response_type", "code"),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope),
pair("prompt", "select_account"),
pair("code_challenge", codeChallenge),
pair("state", state),
pair("code_challenge_method", "S256")
)));
String code = session.waitFor();
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
mapOf(pair("client_id", options.callback.getClientId()),
pair("response_type", "code"),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope),
pair("prompt", "select_account"),
pair("code_challenge", codeChallenge),
pair("state", state),
pair("code_challenge_method", "S256")
)));
String code = session.waitFor();
// Authorization Code -> Token
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
.form(pair("client_id", options.callback.getClientId()),
pair("code", code),
pair("grant_type", "authorization_code"),
pair("code_verifier", codeVerifier),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope))
.ignoreHttpCode()
.retry(5)
.getJson(AuthorizationResponse.class);
handleErrorResponse(response);
return new Result(response.accessToken, response.refreshToken);
// Authorization Code -> Token
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
.form(pair("client_id", options.callback.getClientId()),
pair("code", code),
pair("grant_type", "authorization_code"),
pair("code_verifier", codeVerifier),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope))
.ignoreHttpCode()
.retry(5)
.getJson(AuthorizationResponse.class);
handleErrorResponse(response);
return new Result(response.accessToken, response.refreshToken);
}
}
private Result authenticateDevice(Options options) throws IOException, InterruptedException, JsonParseException, AuthenticationException {
@@ -234,7 +234,7 @@ public class OAuth {
}
}
public interface Session {
public interface Session extends AutoCloseable {
String getState();
String getCodeVerifier();
@@ -253,6 +253,9 @@ public class OAuth {
default String getIdToken() {
return null;
}
@Override
void close();
}
public interface Callback {
@@ -37,40 +37,45 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Result> {
private static final String JAVA_LIST_URL = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
private final DownloadProvider downloadProvider;
private final Path target;
private final Path tempDir;
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) {
public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, Path tempDir, GameJavaVersion javaVersion, String platform) {
this.target = target;
this.tempDir = tempDir;
this.downloadProvider = downloadProvider;
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
.thenComposeAsync(javaDownloadsJson -> {
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(JAVA_LIST_URL))
.thenComposeAsync(javaDownloadsJson -> {
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);
Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
if (osDownloads == null || !osDownloads.containsKey(javaVersion.component()))
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.component());
for (MojangJavaDownloads.JavaDownload download : candidates) {
if (JavaInfo.parseVersion(download.getVersion().getName()) >= javaVersion.majorVersion()) {
this.download = download;
return new GetTask(downloadProvider.injectURLWithCandidates(download.getManifest().getUrl()));
}
}
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
})
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.downloads().get(platform);
if (osDownloads == null || !osDownloads.containsKey(javaVersion.component()))
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.component());
for (MojangJavaDownloads.JavaDownload download : candidates) {
if (JavaInfo.parseVersion(download.version().name()) >= javaVersion.majorVersion()) {
this.download = download;
return new GetTask(downloadProvider.injectURLWithCandidates(download.manifest().getUrl()));
}
}
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
})
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
}
@Override
@@ -81,10 +86,8 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
@Override
public void execute() throws Exception {
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());
Path dest = tempDir.resolve(entry.getKey());
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile file) {
// Use local file if it already exists
try {
BasicFileAttributes localFileAttributes = Files.readAttributes(dest, BasicFileAttributes.class);
@@ -101,17 +104,34 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma");
Path tempFile = target.resolve(entry.getKey() + ".lzma");
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
DownloadInfo raw = file.getDownloads().get("raw");
String rawSha1;
if (raw != null && raw.getSha1() != null) {
rawSha1 = raw.getSha1();
} else {
rawSha1 = null;
}
Path tempFile = tempDir.resolve(entry.getKey() + ".lzma");
var 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 = target.resolve(entry.getKey() + ".tmp");
try (LZMAInputStream input = new LZMAInputStream(Files.newInputStream(tempFile))) {
Path decompressed = tempDir.resolve(entry.getKey() + ".tmp");
var digest = MessageDigest.getInstance("SHA-1");
try (var input = new DigestInputStream(new LZMAInputStream(Files.newInputStream(tempFile)), digest)) {
Files.copy(input, decompressed, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new ArtifactMalformedException("File " + entry.getKey() + " is malformed", e);
}
String actualSha1 = HexFormat.of().formatHex(digest.digest());
if (rawSha1 != null && !actualSha1.equalsIgnoreCase(rawSha1)) {
throw new ArtifactMalformedException("File " + entry.getKey() + " has incorrect SHA-1 hash: expected " + rawSha1 + ", got " + actualSha1);
}
try {
Files.deleteIfExists(tempFile);
} catch (IOException e) {
@@ -137,8 +157,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
}
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteDirectory) {
Files.createDirectories(dest);
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink) {
MojangJavaRemoteFiles.RemoteLink link = ((MojangJavaRemoteFiles.RemoteLink) entry.getValue());
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink link) {
Files.deleteIfExists(dest);
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
}
@@ -157,16 +176,19 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
@Override
public void postExecute() throws Exception {
setResult(new Result(download, javaDownloadsTask.getResult()));
}
if (isDependenciesSucceeded()) {
FileUtils.cleanDirectory(target);
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;
if (Files.getFileStore(target).equals(Files.getFileStore(tempDir))) {
Files.move(tempDir, target, StandardCopyOption.REPLACE_EXISTING);
} else {
FileUtils.copyDirectory(tempDir, target);
FileUtils.deleteDirectory(tempDir);
}
setResult(new Result(download, javaDownloadsTask.getResult()));
}
}
public record Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
}
}
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.download.java.mojang;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import java.lang.reflect.Type;
import java.util.List;
@@ -29,21 +29,10 @@ import java.util.Map;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
@JsonSerializable
@JsonAdapter(MojangJavaDownloads.Adapter.class)
public class MojangJavaDownloads {
private final Map<String, Map<String, List<JavaDownload>>> downloads;
public MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
this.downloads = downloads;
}
public Map<String, Map<String, List<JavaDownload>>> getDownloads() {
return downloads;
}
public record MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
public static class Adapter implements JsonSerializer<MojangJavaDownloads>, JsonDeserializer<MojangJavaDownloads> {
@Override
public JsonElement serialize(MojangJavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.downloads);
@@ -55,78 +44,15 @@ public class MojangJavaDownloads {
}
}
@Immutable
public static class JavaDownload {
private final Availability availability;
private final DownloadInfo manifest;
private final Version version;
public JavaDownload() {
this(new Availability(), new DownloadInfo(), new Version());
}
public JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
this.availability = availability;
this.manifest = manifest;
this.version = version;
}
public Availability getAvailability() {
return availability;
}
public DownloadInfo getManifest() {
return manifest;
}
public Version getVersion() {
return version;
}
@JsonSerializable
public record JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
}
@Immutable
public static class Availability {
private final int group;
private final int progress;
public Availability() {
this(0, 0);
}
public Availability(int group, int progress) {
this.group = group;
this.progress = progress;
}
public int getGroup() {
return group;
}
public int getProgress() {
return progress;
}
@JsonSerializable
public record Availability(int group, int progress) {
}
@Immutable
public static class Version {
private final String name;
private final String released;
public Version() {
this("", "");
}
public Version(String name, String released) {
this.name = name;
this.released = released;
}
public String getName() {
return name;
}
public String getReleased() {
return released;
}
@JsonSerializable
public record Version(String name, String released) {
}
}
@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import java.util.Collections;
import java.util.List;
@@ -75,16 +76,38 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
String mcVersion;
try {
int si1 = version.indexOf('.'), si2 = version.indexOf('.', version.indexOf('.') + 1);
int si1 = version.indexOf('.');
int si2 = version.indexOf('.', si1 + 1);
if (si1 < 0 || si2 < 0) {
LOG.warning("Unsupported NeoForge version: " + version);
continue;
}
int majorVersion = Integer.parseInt(version.substring(0, si1));
if (majorVersion == 0) { // Snapshot version.
mcVersion = version.substring(si1 + 1, si2);
} else {
String ver = version.substring(0, Integer.parseInt(version.substring(si1 + 1, si2)) == 0 ? si1 : si2);
if (majorVersion >= 26) {
int si3 = version.indexOf('.', si2 + 1);
if (si3 < 0) {
LOG.warning("Unsupported NeoForge version: " + version);
continue;
}
String ver = Integer.parseInt(version.substring(si2 + 1, si3)) == 0
? version.substring(0, si2)
: version.substring(0, si3);
int separator = version.indexOf('+');
mcVersion = separator < 0 ? ver : ver + "-" + version.substring(separator + 1);
if (separator < 0)
mcVersion = ver;
else
mcVersion = ver + "-" + version.substring(separator + 1);
} else {
String ver = Integer.parseInt(version.substring(si1 + 1, si2)) == 0
? version.substring(0, si1)
: version.substring(0, si2);
mcVersion = "1." + ver;
}
}
@@ -106,14 +129,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
});
}
private static final class OfficialAPIResult {
private final boolean isSnapshot;
private final List<String> versions;
public OfficialAPIResult(boolean isSnapshot, List<String> versions) {
this.isSnapshot = isSnapshot;
this.versions = versions;
}
@JsonSerializable
private record OfficialAPIResult(boolean isSnapshot, List<String> versions) {
}
}
@@ -0,0 +1,33 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import java.util.Locale;
/// The Graphics API.
public enum GraphicsAPI {
DEFAULT,
OPENGL,
VULKAN;
private final String minecraftArg = name().toLowerCase(Locale.ROOT);
public String getMinecraftArg() {
return minecraftArg;
}
}
@@ -56,6 +56,7 @@ public class LaunchOptions implements Serializable {
private NativesDirectoryType nativesDirType;
private String nativesDir;
private ProcessPriority processPriority = ProcessPriority.NORMAL;
private GraphicsAPI graphicsBackend = GraphicsAPI.DEFAULT;
private Renderer renderer = Renderer.DEFAULT;
private boolean useNativeGLFW;
private boolean useNativeOpenAL;
@@ -244,7 +245,11 @@ public class LaunchOptions implements Serializable {
return processPriority;
}
public Renderer getRenderer() {
public @NotNull GraphicsAPI getGraphicsBackend() {
return graphicsBackend;
}
public @NotNull Renderer getRenderer() {
return renderer;
}
@@ -435,8 +440,13 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setRenderer(@NotNull Renderer renderer) {
options.renderer = renderer;
public Builder setGraphicsBackend(GraphicsAPI backend) {
options.graphicsBackend = Objects.requireNonNullElse(backend, GraphicsAPI.DEFAULT);
return this;
}
public Builder setRenderer(Renderer renderer) {
options.renderer = Objects.requireNonNullElse(renderer, Renderer.DEFAULT);
return this;
}
@@ -17,9 +17,529 @@
*/
package org.jackhuang.hmcl.game;
public enum Renderer {
DEFAULT,
ZINK,
LLVMPIPE,
D3D12
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
@NotNullByDefault
public sealed interface Renderer permits Renderer.Default, Renderer.Driver, Renderer.Unknown {
/// Default renderer.
Default DEFAULT = new Default();
/// Parse a renderer from a string.
static Renderer of(String name) {
if ("DEFAULT".equalsIgnoreCase(name) || name.isBlank())
return DEFAULT;
String upper = name.toUpperCase(Locale.ROOT).trim();
try {
return OpenGL.valueOf(upper);
} catch (IllegalArgumentException ignored) {
}
try {
return Vulkan.valueOf(upper);
} catch (IllegalArgumentException ignored) {
}
return new Unknown(name);
}
/// Get all supported renderers for a given graphics API.
static @Unmodifiable List<Renderer> getSupported(GraphicsAPI api) {
return switch (api) {
case DEFAULT -> List.of(DEFAULT);
case VULKAN -> Vulkan.Holder.SUPPORTED;
case OPENGL -> OpenGL.SUPPORTED;
};
}
String name();
final class Default implements Renderer {
private Default() {
}
@Override
public String name() {
return "DEFAULT";
}
}
sealed interface Driver extends Renderer permits Vulkan, OpenGL {
/// The graphics API that this driver belongs to.
GraphicsAPI api();
/// If this driver can be loaded via [mesa-loader-windows](https://github.com/HMCL-dev/mesa-loader-windows),
/// return the driver name, otherwise return `null`.
@Contract(pure = true)
default @Nullable String mesaDriverName() {
return null;
}
}
enum Vulkan implements Driver {
/// Mesa Lavapipe driver.
///
/// It is a software rasterizer.
///
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
LAVAPIPE("lvp") {
@Override
public String mesaDriverName() {
return "lavapipe";
}
},
/// Mesa Dozen driver.
///
/// It is a Vulkan driver based on DirectX 12.
///
/// ## Note
/// Currently, Dozen does not support the VK_KHR_push_descriptor feature, so it cannot launch Minecraft 26.2
/// Using Dozen can run Minecraft 1.21.11 + VulkanMod, but it will cause the game to crash after playing for a while
DOZEN("dzn") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.WINDOWS;
}
@Override
public String mesaDriverName() {
return "dzn";
}
},
/// NVIDIA Vulkan driver.
///
/// It is a Vulkan driver for NVIDIA GPUs.
///
/// @see <a href="https://developer.nvidia.com/vulkan">Vulkan Open Standard Modern GPU API | NVIDIA Developer</a>
NVIDIA_VULKAN("nvidia") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
}
},
/// Mesa NVK driver.
///
/// It is a Vulkan driver for NVIDIA GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/nvk.html">NVK - The Mesa 3D Graphics Library</a>
NVIDIA_NVK("nouveau") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
}
},
/// AMD Open Source Driver for Vulkan
///
/// It is a Vulkan driver for AMD GPUs.
///
/// @see <a href="https://github.com/GPUOpen-Drivers/AMDVLK">GPUOpen-Drivers/AMDVLK - GitHub</a>
AMDVLK("amd") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.AMD);
}
},
/// Mesa RADV driver.
///
/// It is a Vulkan driver for AMD GCN/RDNA GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/radv.html">RADV - The Mesa 3D Graphics Library</a>
AMD_RADV("radeon") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return Vulkan.hasCard(cards, HardwareVendor.AMD);
}
},
/// Intel Vulkan driver.
///
/// It is a Vulkan driver for Intel GPUs.
INTEL_VULKAN("ig") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
}
},
/// Mesa ANV driver.
///
/// It is a Vulkan driver for Intel GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/anv.html">ANV - The Mesa 3D Graphics Library</a>
INTEL_ANV("intel") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() != OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
}
},
/// Intel HASVK driver.
///
/// It is a Vulkan driver for Intel Gen7 (Ivy Bridge / Haswell) and Gen8 (Broadwell) graphics.
///
/// @see <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/18208">intel: split vulkan driver between gfx7/8 and above</a>
INTEL_HASVK("intel_hasvk") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() != OperatingSystem.WINDOWS
&& cards != null
&& cards.stream().anyMatch(card -> card.getVendor() == HardwareVendor.INTEL && card.getName().startsWith("Intel HD Graphics "));
}
},
/// Qualcomm Vulkan driver.
///
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
QUALCOMM("qc") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.equals(Platform.WINDOWS_ARM64);
}
},
/// Mesa Turnip driver.
///
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/freedreno.html">Freedreno - The Mesa 3D Graphics Library</a>
TURNIP("freedreno") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() != OperatingSystem.WINDOWS && platform.arch().isArm();
}
},
/// MoltenVK driver.
///
/// It is a Vulkan driver for macOS, iOS, tvOS, and visionOS.
///
/// @see <a href="https://github.com/KhronosGroup/MoltenVK">KhronosGroup/MoltenVK - GitHub</a>
MOLTENVK("MoltenVK") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.MACOS;
}
},
/// Mesa KosmicKrisp driver.
///
/// It is a Vulkan driver for Apple Silicon hardware.
///
/// @see <a href="https://docs.mesa3d.org/drivers/kosmickrisp.html">KosmicKrisp - The Mesa 3D Graphics Library</a>
KOSMICKRISP("kosmickrisp_mesa") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.MACOS && platform.arch() == Architecture.ARM64;
}
},
/// Mesa PowerVR driver.
///
/// It is a Vulkan driver for Imagination Technologies PowerVR GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/powervr.html">PowerVR - The Mesa 3D Graphics Library</a>
POWERVR("powervr") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.IMG);
}
},
/// Mesa PanVK driver.
///
/// It is a Vulkan driver for ARM Mali GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/panfrost.html">Panfrost - The Mesa 3D Graphics Library</a>
PANVK("panfrost") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm();
}
},
/// Mesa V3DV driver.
///
/// It is a Vulkan driver for the Raspberry Pi 4 and Raspberry Pi 5.
///
/// @see <a href="https://docs.mesa3d.org/drivers/v3dv.html">V3DV - The Mesa 3D Graphics Library</a>
V3DV("broadcom") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm()
&& Vulkan.hasCard(cards, HardwareVendor.BROADCOM);
}
};
private static final class Holder {
static final List<Renderer> SUPPORTED;
static final Map<Vulkan, Path> DRIVER_TO_ICD_FILE;
static {
var driverToIcdFile = new EnumMap<Vulkan, Path>(Vulkan.class);
var supported = new LinkedHashSet<Renderer>();
// Helper
String archName = switch (Architecture.SYSTEM_ARCH) {
case X86 -> "i686";
case X86_64 -> "x86_64";
case ARM64 -> "aarch64";
default -> Architecture.SYSTEM_ARCH.getCheckedName();
};
var icdFileNamePattern = Pattern.compile("(?<name>[a-zA-Z0-9_-]+)_icd(?:\\." + Pattern.quote(archName) + ")?\\.json");
Map<String, Vulkan> icdNameToDriver = Stream.of(values()).collect(Collectors.toMap(Vulkan::icdName, Function.identity()));
supported.add(DEFAULT);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
supported.addAll(List.of(LAVAPIPE, DOZEN));
List<GraphicsCard> graphicsCards = SystemInfo.getGraphicsCards();
if (graphicsCards != null) {
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
for (GraphicsCard card : graphicsCards) {
if (!card.getVulkanDriverFiles().isEmpty()) {
for (Path icdFile : card.getVulkanDriverFiles()) {
String fileName = FileUtils.getName(icdFile);
if (!fileName.endsWith(".json"))
continue;
Vulkan driver;
Matcher matcher = icdFileNamePattern.matcher(fileName);
if (matcher.matches()) {
String icdName = matcher.group("name");
driver = icdNameToDriver.get(icdName);
} else {
switch (fileName.substring(0, fileName.length() - ".json".length())) {
case "igvk64" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
driver = INTEL_VULKAN;
else
continue;
}
case "igvk32" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
driver = INTEL_VULKAN;
else
continue;
}
case "nv-vk64" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
driver = NVIDIA_VULKAN;
else
continue;
}
case "nv-vk32" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
driver = NVIDIA_VULKAN;
else
continue;
}
case "amd-vulkan64" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
driver = AMDVLK;
else
continue;
}
case "amd-vulkan32" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
driver = AMDVLK;
else
continue;
}
case "qcvk_icd_arm64x" -> {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
driver = QUALCOMM;
else
continue;
}
default -> {
continue;
}
}
}
driverToIcdFile.putIfAbsent(driver, icdFile);
foundSupported.add(driver);
}
}
}
supported.addAll(foundSupported);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
// LWJGL integrates MoltenVK, so it is always available
supported.add(MOLTENVK);
// We need libvulkan.1.dylib to load custom Vulkan drivers
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
Path kosmickrispIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/kosmickrisp_mesa_icd." + archName + ".json");
if (Files.isRegularFile(kosmickrispIcd)) {
driverToIcdFile.put(KOSMICKRISP, kosmickrispIcd);
supported.add(KOSMICKRISP);
}
Path lvpIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/lvp_icd." + archName + ".json");
if (Files.isRegularFile(lvpIcd)) {
driverToIcdFile.put(LAVAPIPE, lvpIcd);
supported.add(LAVAPIPE);
}
}
} else {
List<Path> icdDirs = switch (OperatingSystem.CURRENT_OS) {
case LINUX -> List.of(
Path.of("/usr/share/vulkan/icd.d"),
Path.of("/etc/vulkan/icd.d")
);
case FREEBSD -> List.of(Path.of("/usr/local/share/vulkan/icd.d"));
default -> List.of();
};
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
for (Path icdDir : icdDirs) {
if (!Files.isDirectory(icdDir))
continue;
try (Stream<Path> stream = Files.list(icdDir)) {
for (Path icdFile : Lang.toIterable(stream)) {
String fileName = icdFile.getFileName().toString();
Matcher matcher = icdFileNamePattern.matcher(fileName);
if (matcher.matches()) {
String icdName = matcher.group("name");
Vulkan driver = icdNameToDriver.get(icdName);
if (driver != null) {
driverToIcdFile.put(driver, icdFile);
if (driver.isSupported(Platform.CURRENT_PLATFORM, SystemInfo.getGraphicsCards())) {
foundSupported.add(driver);
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to read Vulkan ICD files in " + icdDir, e);
}
}
supported.addAll(foundSupported);
}
SUPPORTED = List.copyOf(supported);
DRIVER_TO_ICD_FILE = Collections.unmodifiableMap(driverToIcdFile);
}
}
private final String icdName;
Vulkan(String icdName) {
this.icdName = icdName;
}
private static boolean hasCard(@Nullable List<GraphicsCard> cards, HardwareVendor vendor) {
return cards != null && cards.stream().anyMatch(card -> card.getVendor() == vendor);
}
@Override
public GraphicsAPI api() {
return GraphicsAPI.VULKAN;
}
@Contract(pure = true)
public String icdName() {
return icdName;
}
/// Get the path to the ICD file for this driver.
///
/// If the ICD file does not exist, return `null`.
@Contract(pure = true)
public @Nullable Path icdFile() {
return Holder.DRIVER_TO_ICD_FILE.get(this);
}
}
enum OpenGL implements Driver {
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
LLVMPIPE {
@Override
public String mesaDriverName() {
return "llvmpipe";
}
},
ZINK {
@Override
public String mesaDriverName() {
return "zink";
}
},
D3D12 {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.WINDOWS;
}
@Override
public String mesaDriverName() {
return "d3d12";
}
};
static final List<Renderer> SUPPORTED =
Stream.concat(
Stream.of(DEFAULT),
Stream.of(OpenGL.values()).filter(it -> it.isSupported(
Platform.CURRENT_PLATFORM,
null // We don't need to pass graphics cards for OpenGL yet
))
).toList();
@Override
public GraphicsAPI api() {
return GraphicsAPI.OPENGL;
}
}
/// Unknown renderer.
record Unknown(String name) implements Renderer {
}
/// Whether this renderer is supported on the given platform and graphics cards.
///
/// If `cards` is `null`, it means that the graphics cards are unknown.
default boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return true;
}
}
@@ -25,14 +25,14 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
/**
@@ -85,12 +85,6 @@ public final class JavaInfo {
return new JavaInfo(Platform.getPlatform(os, arch), javaVersion, vendor);
}
public static JavaInfo fromReleaseFile(Path releaseFile) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(releaseFile)) {
return fromReleaseFile(reader);
}
}
public static <F, E extends ArchiveEntry> JavaInfo fromArchive(ArchiveFileTree<F, E> tree) throws IOException {
if (tree.getRoot().getSubDirs().size() != 1 || !tree.getRoot().getFiles().isEmpty())
throw new IOException();
@@ -116,24 +110,40 @@ public final class JavaInfo {
if (vendor == null)
return null;
switch (vendor) {
case "N/A":
return null;
case "Oracle Corporation":
return "Oracle";
case "Azul Systems, Inc.":
return "Azul";
case "IBM Corporation":
case "International Business Machines Corporation":
case "Eclipse OpenJ9":
return "IBM";
case "Eclipse Adoptium":
return "Adoptium";
case "Amazon.com Inc.":
return "Amazon";
default:
return vendor;
return switch (vendor) {
case "N/A" -> null;
case "Oracle Corporation" -> "Oracle";
case "Azul Systems, Inc." -> "Azul";
case "IBM Corporation", "International Business Machines Corporation", "Eclipse OpenJ9" -> "IBM";
case "Eclipse Adoptium" -> "Adoptium";
case "Amazon.com Inc." -> "Amazon";
default -> vendor;
};
}
public static final class Builder {
private final Platform platform;
private final String version;
private @Nullable String vendor;
public Builder(Platform platform, String version) {
this.platform = platform;
this.version = version;
}
@Contract("_ -> this")
public Builder setVendor(@Nullable String vendor) {
this.vendor = vendor;
return this;
}
public JavaInfo build() {
return new JavaInfo(platform, version, vendor);
}
}
public static Builder newBuilder(@NotNull Platform platform, @NotNull String version) {
return new Builder(platform, version);
}
public static final JavaInfo CURRENT_ENVIRONMENT = new JavaInfo(Platform.CURRENT_PLATFORM, System.getProperty("java.version"), System.getProperty("java.vendor"));
@@ -34,7 +34,7 @@ public interface JavaRepository {
Path getManifestFile(Platform platform, String name);
Collection<JavaRuntime> getAllJava(Platform platform);
Collection<Path> getAllJava(Platform platform);
Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion);
@@ -27,7 +27,9 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.Charset;
@@ -103,7 +105,7 @@ public class DefaultLauncher extends Launcher {
// Executable
if (StringUtils.isNotBlank(options.getWrapper()))
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars()));
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars(nativeFolder)));
res.add(options.getJava().getBinary().toString());
@@ -264,6 +266,21 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& options.getRenderer() instanceof Renderer.Driver renderer
&& renderer.mesaDriverName() != null) {
res.addDefault("-Dorg.glavo.mesa.loader.nativeDir=", FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader")));
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
&& options.getRenderer() instanceof Renderer.Vulkan vulkanDriver
&& vulkanDriver.icdFile() != null) {
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
res.addDefault("-Dorg.lwjgl.vulkan.libname=", FileUtils.getAbsolutePath(HomebrewUtils.LIB_VULKAN));
}
}
Set<String> classpath = repository.getClasspath(version);
if (analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) {
@@ -296,7 +313,36 @@ public class DefaultLauncher extends Launcher {
}
configuration.put("${natives_directory}", nativeFolderPath);
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
Path javaNativeFolder = FileUtils.toAbsolute(nativeFolder);
@Nullable List<Argument> jvmArguments = version.getArguments().map(Arguments::getJvm).orElse(null);
if (jvmArguments != null) {
for (Argument jvmArgument : jvmArguments) {
if (jvmArgument instanceof StringArgument stringArgument
&& stringArgument.getArgument().startsWith("-Djava.library.path=")) {
// We conservatively handle parameters like "-Djava.library.path=${natives_directory}/java"
// to avoid extracting native libraries to unexpected locations.
String prefix = "-Djava.library.path=${natives_directory}/";
if (stringArgument.getArgument().startsWith(prefix)) {
try {
String subDir = stringArgument.getArgument().substring(prefix.length());
Path actualNativeFolder = FileUtils.toAbsolute(javaNativeFolder.resolve(subDir));
if (actualNativeFolder.startsWith(javaNativeFolder)) {
javaNativeFolder = actualNativeFolder;
}
} catch (IllegalArgumentException ignored) {
}
}
break;
}
}
}
res.addAll(Arguments.parseArguments(Objects.requireNonNullElseGet(jvmArguments, this::getDefaultJVMArguments), configuration));
Arguments argumentsFromAuthInfo = authInfo.getLaunchArguments(options);
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getJvm() != null && !argumentsFromAuthInfo.getJvm().isEmpty())
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getJvm(), configuration));
@@ -305,6 +351,10 @@ public class DefaultLauncher extends Launcher {
res.add("-javaagent:" + javaAgent);
}
if (version.getMainClass() == null) {
throw new IllegalStateException("Main class is null for instance " + version.getId());
}
res.add(version.getMainClass());
res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(ArrayList::new), configuration));
@@ -361,10 +411,16 @@ public class DefaultLauncher extends Launcher {
}
}
if (options.getGraphicsBackend() != GraphicsAPI.DEFAULT
&& gameVersion.isPresent() && GameVersionNumber.compare(gameVersion.get(), "26.2-snapshot-2") >= 0) {
res.add("--graphicsBackend");
res.add(options.getGraphicsBackend().getMinecraftArg());
}
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return new Command(res, tempNativeFolder, encoding);
return new Command(res, tempNativeFolder, javaNativeFolder, encoding);
}
public Map<String, Boolean> getFeatures() {
@@ -401,6 +457,8 @@ public class DefaultLauncher extends Launcher {
}
public void decompressNatives(Path destination) throws NotDecompressingNativesException {
LOG.info("Decompress native libraries to " + destination);
try {
FileUtils.cleanDirectoryQuietly(destination);
for (Library library : version.getLibraries())
@@ -519,7 +577,7 @@ public class DefaultLauncher extends Launcher {
}
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
decompressNatives(nativeFolder);
decompressNatives(command.javaNativeFolder);
}
if (isUsingLog4j())
@@ -528,8 +586,8 @@ public class DefaultLauncher extends Launcher {
Path runDirectory = repository.getRunDirectory(version.getId());
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars())).directory(runDirectory.toFile());
builder.environment().putAll(getEnvVars());
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars(nativeFolder))).directory(runDirectory.toFile());
builder.environment().putAll(getEnvVars(nativeFolder));
SystemUtils.callExternalProcess(builder);
}
@@ -542,7 +600,7 @@ public class DefaultLauncher extends Launcher {
Path appdata = options.getGameDir().toAbsolutePath().getParent();
if (appdata != null) builder.environment().put("APPDATA", appdata.toString());
builder.environment().putAll(getEnvVars());
builder.environment().putAll(getEnvVars(nativeFolder));
process = builder.start();
} catch (IOException e) {
throw new ProcessCreationException(e);
@@ -550,12 +608,13 @@ public class DefaultLauncher extends Launcher {
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
if (listener != null)
startMonitors(p, listener, command.encoding, daemon);
startMonitors(p, nativeFolder, listener, command.encoding, daemon);
return p;
}
private Map<String, String> getEnvVars() {
private Map<String, String> getEnvVars(Path nativeFolder) {
String versionName = Optional.ofNullable(options.getVersionName()).orElse(version.getId());
Map<String, String> env = new LinkedHashMap<>();
env.put("INST_NAME", versionName);
env.put("INST_ID", versionName);
@@ -563,18 +622,32 @@ public class DefaultLauncher extends Launcher {
env.put("INST_MC_DIR", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId())));
env.put("INST_JAVA", options.getJava().getBinary().toString());
Renderer renderer = options.getRenderer();
if (renderer != Renderer.DEFAULT) {
if (options.getRenderer() instanceof Renderer.Driver driver) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (renderer != Renderer.LLVMPIPE)
env.put("GALLIUM_DRIVER", renderer.name().toLowerCase(Locale.ROOT));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
switch (renderer) {
case LLVMPIPE:
if (driver.mesaDriverName() != null) {
if (driver instanceof Renderer.OpenGL && driver != Renderer.OpenGL.LLVMPIPE)
env.put("GALLIUM_DRIVER", driver.mesaDriverName());
else if (driver instanceof Renderer.Vulkan vulkanDriver) {
String icdFile = FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader/" + vulkanDriver.icdName() + "_icd.json"));
env.put("VK_ICD_FILENAMES", icdFile);
env.put("VK_DRIVER_FILES", icdFile);
}
} else if (driver instanceof Renderer.Vulkan vulkanDriver
&& vulkanDriver.icdFile() != null
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
String icdFile = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
env.put("VK_ICD_FILENAMES", icdFile);
env.put("VK_DRIVER_FILES", icdFile);
}
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
if (driver instanceof Renderer.OpenGL oglDriver) {
if (oglDriver == Renderer.OpenGL.LLVMPIPE) {
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
env.put("LIBGL_ALWAYS_SOFTWARE", "1");
break;
case ZINK:
} else if (oglDriver == Renderer.OpenGL.ZINK) {
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
env.put("MESA_LOADER_DRIVER_OVERRIDE", "zink");
/*
* The amdgpu DDX is missing support for modifiers, causing Zink to fail.
@@ -583,7 +656,23 @@ public class DefaultLauncher extends Launcher {
* Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093
*/
env.put("LIBGL_KOPPER_DRI2", "1");
break;
}
} else if (driver instanceof Renderer.Vulkan vulkanDriver
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
if (vulkanDriver.icdFile() != null) {
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
env.put("VK_ICD_FILENAMES", absolutePath);
env.put("VK_DRIVER_FILES", absolutePath);
}
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
if (driver instanceof Renderer.Vulkan vulkanDriver
&& vulkanDriver != Renderer.Vulkan.MOLTENVK
&& vulkanDriver.icdFile() != null) {
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
env.put("VK_ICD_FILENAMES", absolutePath);
env.put("VK_DRIVER_FILES", absolutePath);
}
}
}
@@ -629,13 +718,6 @@ public class DefaultLauncher extends Launcher {
nativeFolder = Path.of(options.getNativesDir());
}
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
decompressNatives(nativeFolder);
}
if (isUsingLog4j())
extractLog4jConfigurationFile();
String scriptExtension = FileUtils.getExtension(scriptFile);
boolean usePowerShell = "ps1".equals(scriptExtension);
@@ -648,7 +730,7 @@ public class DefaultLauncher extends Launcher {
final Command commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.commandLine.toString();
Map<String, String> envVars = getEnvVars();
Map<String, String> envVars = getEnvVars(nativeFolder);
if (isWindows && !usePowerShell) {
// https://stackoverflow.com/a/28452546
@@ -658,6 +740,12 @@ public class DefaultLauncher extends Launcher {
}
}
if (isUsingLog4j())
extractLog4jConfigurationFile();
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER)
decompressNatives(commandLine.javaNativeFolder);
Files.createDirectories(scriptFile.getParent());
try (OutputStream outputStream = Files.newOutputStream(scriptFile)) {
@@ -785,7 +873,7 @@ public class DefaultLauncher extends Launcher {
throw new ExecutionPolicyLimitException();
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, Charset encoding, boolean isDaemon) {
private void startMonitors(ManagedProcess managedProcess, Path nativeFolder, ProcessListener processListener, Charset encoding, boolean isDaemon) {
processListener.setProcess(managedProcess);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
processListener.onLog(it, false);
@@ -802,8 +890,8 @@ public class DefaultLauncher extends Launcher {
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
try {
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars())).directory(options.getGameDir().toFile());
builder.environment().putAll(getEnvVars());
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars(nativeFolder))).directory(options.getGameDir().toFile());
builder.environment().putAll(getEnvVars(nativeFolder));
SystemUtils.callExternalProcess(builder);
} catch (Throwable e) {
LOG.warning("An Exception happened while running exit command.", e);
@@ -812,15 +900,10 @@ public class DefaultLauncher extends Launcher {
}), "exit-waiter", isDaemon));
}
private static final class Command {
final CommandBuilder commandLine;
final Path tempNativeFolder;
final Charset encoding;
Command(CommandBuilder commandBuilder, Path tempNativeFolder, Charset encoding) {
this.commandLine = commandBuilder;
this.tempNativeFolder = tempNativeFolder;
this.encoding = encoding;
}
private record Command(
CommandBuilder commandLine,
@Nullable Path tempNativeFolder,
Path javaNativeFolder,
Charset encoding) {
}
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -585,7 +586,7 @@ public class CurseAddon implements RemoteMod.IMod {
}
return RemoteMod.Dependency.ofGeneral(RELATION_TYPE.get(dependency.getRelationType()), CurseForgeRemoteModRepository.MODS, Integer.toString(dependency.getModId()));
}).distinct().filter(Objects::nonNull).collect(Collectors.toList()),
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
gameVersions.stream().filter(GameVersionNumber::isKnown).toList(),
gameVersions.stream().flatMap(version -> {
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
@@ -293,7 +293,7 @@ public final class ForgeNewModMetadata {
throw new IOException();
}
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) throws IOException {
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) {
List<HashMap<String, Object>> dependencies = null;
try {
dependencies = toml.getList("dependencies." + modID);
@@ -332,11 +332,11 @@ public final class ForgeNewModMetadata {
}
}
if (result == loader)
if (result != null) {
if (result != loader)
LOG.warning("Loader mismatch for mod " + modID + ", found " + result + ", expecting " + loader);
return result;
else if (result != null)
throw new IOException("Loader mismatch");
else {
} else {
LOG.warning("Cannot determine the mod loader for mod " + modID + ", expected " + loader);
return loader;
}
@@ -86,6 +86,10 @@ public enum Architecture {
return this == X86 || this == X86_64;
}
public boolean isArm() {
return this == ARM32 || this == ARM64;
}
public static final Architecture CURRENT_ARCH;
public static final Architecture SYSTEM_ARCH;
@@ -18,18 +18,22 @@
package org.jackhuang.hmcl.util.platform.hardware;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.Unmodifiable;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Glavo
*/
/// @author Glavo
@NotNullByDefault
public final class GraphicsCard {
public static String cleanName(String name) {
@UnknownNullability
public static String cleanName(@UnknownNullability String name) {
if (name == null)
return null;
@@ -58,13 +62,15 @@ public final class GraphicsCard {
private final @Nullable Type type;
private final @Nullable String driver;
private final @Nullable String driverVersion;
private final @Unmodifiable List<Path> vulkanDriverFiles;
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion) {
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion, List<Path> vulkanDriverFiles) {
this.name = Objects.requireNonNull(name);
this.vendor = vendor;
this.type = type;
this.driver = driver;
this.driverVersion = driverVersion;
this.vulkanDriverFiles = vulkanDriverFiles;
}
public String getName() {
@@ -79,6 +85,10 @@ public final class GraphicsCard {
return driverVersion;
}
public List<Path> getVulkanDriverFiles() {
return vulkanDriverFiles;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(name);
@@ -96,11 +106,12 @@ public final class GraphicsCard {
}
public static final class Builder {
private String name;
private HardwareVendor vendor;
private Type type;
private String driver;
private String driverVersion;
private @Nullable String name;
private @Nullable HardwareVendor vendor;
private @Nullable Type type;
private @Nullable String driver;
private @Nullable String driverVersion;
private @Nullable List<Path> vulkanDriverFiles;
public GraphicsCard build() {
String name = this.name;
@@ -111,10 +122,12 @@ public final class GraphicsCard {
name = "Unknown";
}
return new GraphicsCard(name, vendor, type, driver, driverVersion);
List<Path> vulkanDriverFiles = Objects.requireNonNullElse(this.vulkanDriverFiles, List.of());
return new GraphicsCard(name, vendor, type, driver, driverVersion, List.copyOf(vulkanDriverFiles));
}
public String getName() {
public @Nullable String getName() {
return name;
}
@@ -123,7 +136,7 @@ public final class GraphicsCard {
return this;
}
public HardwareVendor getVendor() {
public @Nullable HardwareVendor getVendor() {
return vendor;
}
@@ -132,7 +145,7 @@ public final class GraphicsCard {
return this;
}
public Type getType() {
public @Nullable Type getType() {
return type;
}
@@ -141,7 +154,7 @@ public final class GraphicsCard {
return this;
}
public String getDriver() {
public @Nullable String getDriver() {
return driver;
}
@@ -150,7 +163,7 @@ public final class GraphicsCard {
return this;
}
public String getDriverVersion() {
public @Nullable String getDriverVersion() {
return driverVersion;
}
@@ -158,5 +171,14 @@ public final class GraphicsCard {
this.driverVersion = driverVersion;
return this;
}
public @Nullable List<Path> getVulkanDriverFiles() {
return vulkanDriverFiles;
}
public Builder setVulkanDriverFiles(@Nullable List<Path> files) {
this.vulkanDriverFiles = files;
return this;
}
}
}
@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.macos;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNullByDefault;
import java.nio.file.Path;
/// @author Glavo
@NotNullByDefault
public final class HomebrewUtils {
/// The path to Homebrew prefix.
///
/// - For macOS x86_64, it is `/usr/local`;
/// - For macOS ARM64, it is `/opt/homebrew`;
/// - For other operating systems, it is undefined.
public static final Path HOMEBREW_PREFIX;
/// The path to `libvulkan.1.dylib`.
///
/// For non-macOS operating systems, it is undefined.
public static final Path LIB_VULKAN;
static {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
// TODO: custom Homebrew prefix for macOS
HOMEBREW_PREFIX = Architecture.SYSTEM_ARCH.isX86()
? Path.of("/usr/local")
: Path.of("/opt/homebrew");
LIB_VULKAN = HOMEBREW_PREFIX.resolve("lib/libvulkan.1.dylib");
} else {
// For other operating systems, we don't need Homebrew.
var placeholder = Path.of("");
HOMEBREW_PREFIX = placeholder;
LIB_VULKAN = placeholder;
}
}
private HomebrewUtils() {
}
}
@@ -28,8 +28,11 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -48,6 +51,25 @@ final class WindowsGPUDetector {
}
}
private static @Nullable List<Path> parseVulkanDriverFiles(Object vulkanDriverFiles) {
if (vulkanDriverFiles instanceof String file) {
try {
return List.of(Path.of(file));
} catch (InvalidPathException e) {
LOG.warning("Failed to parse Vulkan ICD file path: " + file, e);
return null;
}
} else if (vulkanDriverFiles instanceof String[] files) {
try {
return Stream.of(files).map(Path::of).toList();
} catch (InvalidPathException e) {
LOG.warning("Failed to parse Vulkan ICD file paths: " + Arrays.toString(files), e);
return null;
}
} else
return null;
}
private static List<GraphicsCard> detectByCim() {
try {
String getCimInstance = OperatingSystem.SYSTEM_VERSION.getVersion().startsWith("6.1")
@@ -116,6 +138,7 @@ final class WindowsGPUDetector {
String vendor = regValueToString(reg.queryValue(hkey, subkey, "ProviderName"));
String driverVersion = regValueToString(reg.queryValue(hkey, subkey, "DriverVersion"));
String dacType = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.DacType"));
Object vulkanDriverName = reg.queryValue(hkey, subkey, "VulkanDriverName");
GraphicsCard.Builder builder = GraphicsCard.builder();
if (name != null)
@@ -126,6 +149,8 @@ final class WindowsGPUDetector {
builder.setDriverVersion(driverVersion);
if (dacType != null)
builder.setType(fromDacType(dacType));
if (vulkanDriverName != null)
builder.setVulkanDriverFiles(parseVulkanDriverFiles(vulkanDriverName));
result.add(builder.build());
}
return result;
@@ -95,6 +95,18 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return VersionRange.atMost(asGameVersion(maximum));
}
/// Determines whether the given version string corresponds to a known Minecraft version.
///
/// If the version string cannot be parsed as any known version type (release, snapshot, pre-release, etc.)
/// and is not a known April Fools snapshot, it is considered an unknown version.
///
/// @param version the version string to check, e.g. `"1.21.4"`, `"25w14craftmine"`, etc.
/// @return {@code true} if the version string corresponds to a known Minecraft version, {@code false} otherwise
/// @see #asGameVersion(String)
public static boolean isKnown(String version) {
return !(asGameVersion(version) instanceof Special special && special.prev == null);
}
final String value;
final String normalized;
@@ -151,7 +163,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
///
/// ```java
/// GameVersionNumber.asVersion("...").isAtLeast("1.13", "17w43a");
///```
/// ```
///
/// @param strictReleaseVersion When `strictReleaseVersion` is `false`, `releaseVersion` is considered less than
/// its corresponding pre/rc versions.
@@ -914,4 +914,6 @@
26.1
26.1.1-rc-1
26.1.1
26.1.2-rc-1
26.1.2
26w14a