Compare commits

...
Author SHA1 Message Date
Glavo 5803235407 使用 WebURL for Java 解析 URI (#6084) 2026-05-12 21:43:19 +08:00
CiiLu afbf567fef 缓解离线皮肤 CSL 输入框打一个字弹窗一个 toast “操作失败” 问题 (#5789) 2026-05-09 21:26:06 +08:00
Glavo 72b3c1981a 支持断点续传 (#4558) 2026-05-09 21:02:27 +08:00
Glavo 72ee82543b Use null-safe converter for LineSelectButton instances (#6075) 2026-05-09 08:54:14 +08:00
Glavo 996c9f4c86 检测到系统 zlib 实现为 zlib-ng 时跳过 Forge/NeoForge 安装时的哈希检测 (#6068) 2026-05-08 12:13:06 +08:00
3gf8jv4dv f8c1e97376 fix(l10n): edit Version List options on Settings > Downloads for zh-hans (#6069) 2026-05-07 21:51:20 +08:00
CiiLu f146040ff3 fix: 版本列表项目点一下 tag 就会消失 (#6067) 2026-05-06 21:02:27 +08:00
辞庐 0da28173ab 优化设置 -> 通用页面 (#6014) 2026-05-04 17:15:38 +08:00
辞庐 be822666a7 导出 MultiMC 整合包时不再写入 MaxMemAlloc (#6032) 2026-05-04 17:12:24 +08:00
辞庐 8c99ee3b0d 优化种子隐藏按钮 (#6036) 2026-05-04 17:03:11 +08:00
Zkitefly 0e3b4a7853 Update link to software name and version number location (#6039) 2026-05-04 16:59:22 +08:00
辞庐 615b1dbaae fix: #5844 整合包实例版本号在特定场景下不显示 (#6054) 2026-05-04 16:53:56 +08:00
辞庐 0fa51f830d 旁观者模式 -> 旁观模式 (#6053) 2026-05-04 15:46:11 +08:00
ShulkerSakura 007bb345d0 Fixed wrong skin review model (#6046) 2026-05-04 15:44:11 +08:00
CalbootOnceMore 46536f11d3 [Chore] Transform FileDownloadTask.IntegrityCheck into record (#6052) 2026-05-03 20:44:10 +08:00
Glavo b1232b5dc8 缩放 8mi-tech.png 到合适的大小 (#6051) 2026-05-03 14:16:12 +08:00
8Mi_Yile f8f047d67e chore: thanks新增 八蓝米科技 (#6045) 2026-05-03 13:51:37 +08:00
辞庐 86529c6f15 清理 SwingFXUtils (#6030) 2026-05-01 11:15:11 +08:00
CalbootOnceMore d63829c344 [Chore] upgrade.RemoteVersion 转换为记录类 (#6029) 2026-04-30 21:37:54 +08:00
辞庐 34359a7e86 输入框 错误提示与确认按钮间添加间距 (#6025) 2026-04-30 20:50:29 +08:00
辞庐 aa335d8365 fix: 世界管理 -> 创建备份 点击后启动器崩溃 (#6023) 2026-04-30 20:49:52 +08:00
辞庐 a565d3b7ce 移除关于页面 “版权” 项超链接 (#6026) 2026-04-30 20:36:38 +08:00
辞庐 3bff3c7f23 邀请码输入框添加非空校验 (#6024) 2026-04-30 12:50:20 +08:00
4e6e832e9c feat: 文件选择器支持选择文件和文件夹 (#5806)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-04-27 21:25:50 +08:00
辞庐 d6648a840e 优化微软登录未购买提示 (#5611) 2026-04-26 21:57:51 +08:00
辞庐 f8c2af716f fix: 主页面更新提示未设置鼠标指针为手形 (#6012) 2026-04-26 20:48:55 +08:00
辞庐 bd812ccb67 优化设置页更新按钮 (#6010) 2026-04-26 20:48:21 +08:00
辞庐 dad190f7e6 添加外置登录账户弹窗使用 JFXHyperLink (#6013) 2026-04-26 20:47:11 +08:00
Wulian233 1b1ba64a38 Fix 5963: 愚人节版本 Minecraft 2.0 的 Legacy Fabric 下载列表为空 (#6009) 2026-04-26 20:46:07 +08:00
辞庐 e31268c39d fix: 微软登录完成页弹窗缺少圆角 (#6006)
https://github.com/HMCL-dev/HMCL/pull/5832
2026-04-26 14:41:58 +08:00
Glavo cb9d32dc91 构建时创建 deb 包 (#5981) 2026-04-26 14:40:05 +08:00
辞庐 36568df5b1 rename NEO_FORGED to NEO_FORGE (#6008) 2026-04-26 13:55:34 +08:00
Glavo b4ac42d789 Add AGENTS.md (#5996) 2026-04-25 12:56:45 +08:00
Glavo 32aafb11a3 增强对 x86-64 转译支持状态的检测 (#5993) 2026-04-24 21:25:08 +08:00
Glavo 36dab70b7a 修复 CurseForge 源中内容不显示 Minecraft 26.x 版本的问题 (#5994) 2026-04-24 20:44:06 +08:00
Glavo ebf253b1a7 当鼠标未悬停在滚动条上时使滚动条半透明 (#5991) 2026-04-23 21:35:27 +08:00
辞庐 539dcca5ca 优化工具栏“全选”功能 (#5867) 2026-04-23 21:24:47 +08:00
neveler 659b6c2a70 优化文档中的版本声明周期示例 (#5920) 2026-04-22 22:08:39 +08:00
Glavo 3dcc7ab311 支持通过环境变量设置 HMCL 配置文件夹 (#5987) 2026-04-22 21:21:46 +08:00
辞庐 e0ccb58272 fix: 更新时不保存窗口位置 (#5819) 2026-04-22 21:12:10 +08:00
XirrenandGlavo d4ec8ebec8 对于jar路径,将感叹号视为非法字符 (#5976)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-22 21:05:20 +08:00
ENC_Euphony f3b21e5099 fix: place datapack in resource packs folder (#5988)
fix #5970
2026-04-22 20:49:05 +08:00
ENC_Euphony 202fd38180 fix: close Files.list stream when reading world archive root (#5986) 2026-04-21 20:36:19 +08:00
Damon LuandGlavo f5768f2f95 在 macOS 下随明暗模式设置 NSAppearance (#5778)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-20 23:21:36 +08:00
3gf8jv4dv 2d377d06de fix(l10n): revise Version List options on Settings > Downloads page (#5974) 2026-04-20 22:13:24 +08:00
辞庐 42d1581ba0 fix: 可输入的 combobox 内容没有与不可输入的对齐 (#5635) 2026-04-20 22:11:56 +08:00
3gf8jv4dv 995a23c2bc fix: restore tip when mod loader version list is empty (#5978) 2026-04-20 22:08:31 +08:00
Calboot 8969f55a1f [Bugfix] 使用 tomlj 代替 toml4j 以适配 TOML 标准 (#5863)
Resolves #5544
2026-04-20 21:35:47 +08:00
neveler 312a285792 HTMLRenderer 支持嵌套列表 (#5912) 2026-04-20 21:31:09 +08:00
Glavo d85b052195 更新 .gitignore 文件以忽略 codex 相关文件 (#5979) 2026-04-20 21:24:07 +08:00
GlavoandCopilot cd0d3d1b97 修复添加默认 JVM 参数时可能覆盖通过外部文件传递的参数的问题 (#5966)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 20:58:13 +08:00
Glavo 6ccc7df998 Fix #5862: 修复 CurseForge Addon Logo 中不包含 thumbnailUrl 时无法加载图标的问题 (#5971) 2026-04-19 22:20:04 +08:00
Glavo 73e5909a69 使用 jwebp 代替 TwelveMonkeys 加载 WebP 图像 (#5968) 2026-04-19 21:06:23 +08:00
辞庐 ade487277d 优化滚动条样式 (#5784) 2026-04-18 22:00:21 +08:00
3gf8jv4dv e755d17977 fix(l10n): revise Download Source options on Settings > Downloads page (#5644) 2026-04-18 21:57:37 +08:00
Damon Lu 292288de9e 改善启动脚本导出 (#5906)
Resolve #5200
2026-04-18 21:45:52 +08:00
Xirren 57f6dec462 整合包完整性检查忽略已升级模组 (#5918)
close #4812
2026-04-18 21:44:34 +08:00
ENC_EuphonyandGlavo 8e1a549c9d feat: modrinth loader tag aggregate display (#5964)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-18 21:38:20 +08:00
辞庐andgemini-code-assist[bot] 1d18eae377 fix: 下载页面部分模组 subtitle 换行 (#5637)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-17 22:20:08 +08:00
辞庐 6ef04d2fa5 对禁止取消的操作禁用取消按钮 (#5948) 2026-04-17 22:12:54 +08:00
ENC_Euphony 1778e9d916 fix: select logs outside the screen was not deselected correctly (#5955)
Fix #5884
2026-04-17 22:08:23 +08:00
Glavo dbb63e9faa 移除 ComponentSublist 内部元素的缩进 (#5956) 2026-04-17 21:24:15 +08:00
Glavo 6b1c3182eb Bump version root to 3.14 (#5961) 2026-04-17 10:42:57 +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
134 changed files with 4271 additions and 1316 deletions

No files matched your search

+6
View File
@@ -48,3 +48,9 @@ jobs:
name: HMCL-${{ env.SHORT_SHA }}-sh
path: HMCL/build/libs/HMCL-*.sh
archive: false
- name: Upload DEB
uses: actions/upload-artifact@v7
with:
name: HMCL-${{ env.SHORT_SHA }}-deb
path: HMCL/build/libs/HMCL-*.deb
archive: false
+11
View File
@@ -46,16 +46,20 @@ jobs:
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb.sha256"
- name: Verify artifacts
if: ${{ env.continue == 'true' }}
run: |
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
export DEB_SHA256=$(cat HMCL-$HMCL_VERSION.deb.sha256 | tr -d '\n')
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
echo "$DEB_SHA256 HMCL-$HMCL_VERSION.deb" | sha256sum -c
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
@@ -67,6 +71,7 @@ jobs:
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.deb]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> RELEASE_NOTE
# CNB Release Note
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
@@ -76,6 +81,8 @@ jobs:
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.deb]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> CNB_RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
@@ -86,6 +93,7 @@ jobs:
"HMCL-${{ env.HMCL_VERSION }}.exe" \
"HMCL-${{ env.HMCL_VERSION }}.jar" \
"HMCL-${{ env.HMCL_VERSION }}.sh" \
"HMCL-${{ env.HMCL_VERSION }}.deb" \
--target "${{ env.HMCL_COMMIT_SHA }}" \
--title "${{ env.HMCL_TAG_NAME }}" \
--notes-file RELEASE_NOTE \
@@ -120,6 +128,9 @@ jobs:
echo "Uploading HMCL-$HMCL_VERSION.sh"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
echo "Uploading HMCL-$HMCL_VERSION.deb"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.deb"
env:
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
CNB_REPO: HMCL-dev/HMCL
+7 -1
View File
@@ -64,4 +64,10 @@ language-subtag-registry
# test
/hmcl.json
/.hmcl.json
/.hmcl/
/.hmcl/
# AI
/.gemini/
!/.gemini/config.yaml
.codex
/.gradle-*/
+27
View File
@@ -0,0 +1,27 @@
# Java Code Style Requirements
These rules apply to all Java code written or modified in this repository.
## Nullability
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability must never be implicit.
## Immutability
- Immutable arrays and collections must be explicitly annotated with JetBrains Annotations `@Unmodifiable` or `@UnmodifiableView` as appropriate.
- For arrays, use type-use syntax such as `String @Unmodifiable []`.
## Documentation
- Every class, field, and method must have documentation.
- Documentation must use `///` Markdown-style Javadoc comments.
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
- Add concise implementation comments inside complex logic whenever they materially improve readability or explain non-obvious behavior.
## Gradle
- When invoking Gradle in this repository, always set `GRADLE_USER_HOME` to the workspace-local `.gradle-user-home` directory.
- Prefer commands such as `./gradlew -g .gradle-user-home ...` or the equivalent environment-variable-based configuration.
- When running Gradle `test` tasks, use a higher timeout of ten minutes.
+39 -10
View File
@@ -6,6 +6,8 @@ import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.pack.CreateDeb
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
@@ -56,7 +58,7 @@ dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.jwebp)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)
@@ -195,7 +197,7 @@ tasks.shadowJar {
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
@@ -251,20 +253,22 @@ tasks.processResources {
}
}
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
inputs.file(jarPath)
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
outputs.files(extensions.map { artifactFile(it) })
doLast {
val jarContent = jarPath.readBytes()
ZipFile(jarPath).use { zipFile ->
for (extension in extensions) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
val output = artifactFile(extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")
@@ -279,8 +283,31 @@ val makeExecutables by tasks.registering {
}
}
val makeDeb by tasks.registering(CreateDeb::class) {
dependsOn(makeExecutables)
val debFile = layout.file(provider { artifactFile("deb") })
val debChannel = when (versionType) {
"stable" -> ReleaseType.STABLE
"dev" -> ReleaseType.DEVELOPMENT
else -> ReleaseType.NIGHTLY
}
version.set(project.version.toString())
releaseType.set(debChannel)
appShFile.set(layout.file(provider { artifactFile("sh") }))
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
outputFile.set(debFile)
doLast {
createChecksum(debFile.get().asFile)
}
}
tasks.build {
dependsOn(makeExecutables)
dependsOn(makeDeb)
}
fun parseToolOptions(options: String?): MutableList<String> {
@@ -377,12 +404,14 @@ val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgra
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
val source = layout.projectDirectory.file("terracotta-template.json");
classifiers.set(listOf(
"windows-x86_64", "windows-arm64",
"macos-x86_64", "macos-arm64",
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
"freebsd-x86_64"
))
classifiers.set(
listOf(
"windows-x86_64", "windows-arm64",
"macos-x86_64", "macos-arm64",
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
"freebsd-x86_64"
)
)
version.set(libs.versions.terracotta)
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,277 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.adapters.skins.ComboBoxListViewSkin;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
protected final ObjectProperty<Paint> promptTextFill;
private boolean invalid = true;
private final StackPane customPane;
private final StackPane line = new StackPane();
private final StackPane focusedLine = new StackPane();
private final Text promptText = new Text();
private final double initScale = 0.05;
private final Scale scale;
private final Timeline linesAnimation;
private ParallelTransition transition;
private CachedTransition promptTextUpTransition;
private CachedTransition promptTextDownTransition;
private CachedTransition promptTextColorTransition;
private final Scale promptTextScale;
private Paint oldPromptTextFill;
private final BooleanBinding usePromptText;
public JFXComboBoxListViewSkin(JFXComboBox<T> comboBox) {
super(comboBox);
this.scale = new Scale(this.initScale, 1.0F);
this.linesAnimation = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.scale.xProperty(), this.initScale, Interpolator.EASE_BOTH), new KeyValue(this.focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1.0F), new KeyValue(this.focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160.0F), new KeyValue(this.scale.xProperty(), 1, Interpolator.EASE_BOTH)));
this.promptTextScale = new Scale(1.0F, 1.0F, 0.0F, 0.0F);
this.promptTextFill = new SimpleObjectProperty(Color.valueOf("#B2B2B2"));
this.usePromptText = Bindings.createBooleanBinding(this::usePromptText, ((JFXComboBox) this.getSkinnable()).valueProperty(), this.getSkinnable().promptTextProperty());
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
this.promptText.textProperty().bind(comboBox.promptTextProperty());
this.promptText.fillProperty().bind(this.promptTextFill);
this.promptText.getStyleClass().addAll("text", "prompt-text");
this.promptText.getTransforms().add(this.promptTextScale);
if (!comboBox.isLabelFloat()) {
this.promptText.visibleProperty().bind(this.usePromptText);
}
this.customPane = new StackPane();
this.customPane.setMouseTransparent(true);
this.customPane.getStyleClass().add("combo-box-button-container");
this.customPane.backgroundProperty().bindBidirectional(this.getSkinnable().backgroundProperty());
this.customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
this.customPane.getChildren().add(this.promptText);
this.getChildren().add(0, this.customPane);
StackPane.setAlignment(this.promptText, Pos.CENTER_LEFT);
this.line.getStyleClass().add("input-line");
this.focusedLine.getStyleClass().add("input-focused-line");
this.getChildren().add(this.line);
this.getChildren().add(this.focusedLine);
this.line.setPrefHeight(1.0F);
this.line.setTranslateY(1.0F);
this.line.setManaged(false);
this.line.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
if (this.getSkinnable().isDisabled()) {
this.line.setBorder(new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1.0F))));
this.line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
}
this.focusedLine.setPrefHeight(2.0F);
this.focusedLine.setTranslateY(0.0F);
this.focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
this.focusedLine.setOpacity(0.0F);
this.focusedLine.getTransforms().add(this.scale);
this.focusedLine.setManaged(false);
if (comboBox.isEditable()) {
comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 8 4 8");
comboBox.getEditor().promptTextProperty().unbind();
comboBox.getEditor().setPromptText(null);
comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> {
this.usePromptText.invalidate();
comboBox.setValue(this.getConverter().fromString(newVal));
});
}
comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.promptText.visibleProperty().unbind();
JFXUtilities.runInFX(() -> this.createFloatingAnimation());
} else {
this.promptText.visibleProperty().bind(this.usePromptText);
}
this.createFocusTransition();
});
comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
this.focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, newVal, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(160.0F));
}
protected void starting() {
super.starting();
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
}
};
this.transition = null;
}
}
});
comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
this.line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
}
});
comboBox.disabledProperty().addListener((o, oldVal, newVal) -> {
this.line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(this.line.getHeight()))) : Border.EMPTY);
this.line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
});
comboBox.focusedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.focus();
} else {
this.unFocus();
}
});
comboBox.valueProperty().addListener((o, oldVal, newVal) -> {
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.animateFloatingLabel(newVal != null && !newVal.toString().isEmpty());
}
});
}
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
this.customPane.resizeRelocate(x, y, w, h);
if (this.invalid) {
this.invalid = false;
if (!this.getSkinnable().isEditable()) {
Text javaPromptText = (Text) super.getDisplayNode().lookup(".text");
if (javaPromptText != null) {
this.promptTextFill.set(javaPromptText.getFill());
}
}
this.createFloatingAnimation();
if (((ComboBoxBase) this.getSkinnable()).getValue() != null) {
this.animateFloatingLabel(true);
}
}
this.focusedLine.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.focusedLine.prefHeight(-1.0F));
this.line.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.line.prefHeight(-1.0F));
this.scale.setPivotX(w / (double) 2.0F);
}
private void createFloatingAnimation() {
this.promptTextUpTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), -this.customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(240.0F));
}
};
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, ((JFXComboBox) this.getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(160.0F));
}
protected void starting() {
super.starting();
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
}
};
this.promptTextDownTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(240.0F));
}
};
this.promptTextDownTransition.setOnFinished((finish) -> {
this.promptText.setTranslateY(0.0F);
this.promptTextScale.setX(1.0F);
this.promptTextScale.setY(1.0F);
});
}
private void focus() {
if (this.transition == null) {
this.createFocusTransition();
}
this.transition.play();
}
private void animateFloatingLabel(boolean up) {
if (this.promptText == null) {
Platform.runLater(() -> this.animateFloatingLabel(up));
} else {
if (this.transition != null) {
this.transition.stop();
this.transition.getChildren().remove(this.promptTextUpTransition);
this.transition.getChildren().remove(this.promptTextColorTransition);
this.transition = null;
}
if (up && this.promptText.getTranslateY() == (double) 0.0F) {
this.promptTextDownTransition.stop();
this.promptTextUpTransition.play();
if (this.getSkinnable().isFocused()) {
this.promptTextColorTransition.play();
}
} else if (!up) {
this.promptTextUpTransition.stop();
if (this.getSkinnable().isFocused()) {
this.promptTextFill.set(this.oldPromptTextFill);
}
this.promptTextDownTransition.play();
}
}
}
private void createFocusTransition() {
this.transition = new ParallelTransition();
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.transition.getChildren().add(this.promptTextUpTransition);
this.transition.getChildren().add(this.promptTextColorTransition);
}
this.transition.getChildren().add(this.linesAnimation);
}
private void unFocus() {
if (this.transition != null) {
this.transition.stop();
}
this.scale.setX(this.initScale);
this.focusedLine.setOpacity(0.0F);
if (((JFXComboBox) this.getSkinnable()).isLabelFloat() && this.oldPromptTextFill != null) {
this.promptTextFill.set(this.oldPromptTextFill);
if (this.usePromptText()) {
this.promptTextDownTransition.play();
}
}
}
private boolean usePromptText() {
Object txt = ((JFXComboBox) this.getSkinnable()).getValue();
String promptTxt = this.getSkinnable().getPromptText();
return (txt == null || txt.toString().isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !this.promptTextFill.get().equals(Color.TRANSPARENT);
}
}
@@ -39,12 +39,10 @@ import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
@@ -138,6 +136,9 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
Themes.applyNativeDarkMode(primaryStage);
UpdateChecker.init();
primaryStage.show();
@@ -343,6 +344,8 @@ public final class Launcher extends Application {
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
}
LOG.info("Zlib Compatible: " + ZlibUtils.IS_ZLIB_COMPATIBLE);
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
launch(Launcher.class, args);
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
@@ -46,7 +45,6 @@ public final class Metadata {
public static final int RECOMMENDED_JAVA_VERSION = 21;
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
@@ -60,34 +58,38 @@ public final class Metadata {
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path CURRENT_DIRECTORY = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_GLOBAL_DIRECTORY;
public static final Path HMCL_CURRENT_DIRECTORY;
public static final Path DEPENDENCIES_DIRECTORY;
static {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
String hmclHome = System.getProperty("hmcl.home", System.getenv("HMCL_USER_HOME"));
if (StringUtils.isBlank(hmclHome)) {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Path.of(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
HMCL_CURRENT_DIRECTORY = StringUtils.isNotBlank(hmclCurrentDir)
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
String hmclDependencies = System.getProperty("hmcl.dependencies.dir", System.getenv("HMCL_DEPENDENCIES_DIR"));
DEPENDENCIES_DIRECTORY = StringUtils.isNotBlank(hmclDependencies)
? Path.of(hmclDependencies).toAbsolutePath().normalize()
: HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
@@ -515,7 +515,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return FileUtils.isNameValid(id);
return FileUtils.isNameValidForJar(id);
}
/**
@@ -72,7 +72,6 @@ import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
@@ -173,11 +172,10 @@ public final class LauncherHelper {
}
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer == null || renderer.getMesaDriverName() == null
|| OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|| renderer.mesaDriverName() == null)
return null;
}
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
@@ -188,7 +186,7 @@ public final class LauncherHelper {
return null;
}
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.getMesaDriverName();
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
@@ -626,7 +624,7 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& isCompatibleWithX86Java()) {
&& Platform.isSupportedTranslationX86_64()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
@@ -31,7 +31,6 @@ public final class Log {
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
@@ -57,14 +56,6 @@ public final class Log {
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
@@ -122,16 +122,27 @@ public final class JavaManager {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
// Windows x86-64 platform is compatible with x86 programs
return architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
// Since Windows 10 Build 21277, Windows Arm64 has been compatible with x86-64 programs via translation
if (architecture == Architecture.X86_64 && Platform.isSupportedTranslationX86_64())
return true;
// Windows Arm64 is compatible with x86 programs via translation
if (architecture == Architecture.X86)
return true;
return false;
}
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
// macOS Arm64 compatible with x86-64 programs via Rosetta 2.
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
return architecture == Architecture.X86_64;
break;
}
@@ -364,13 +375,13 @@ public final class JavaManager {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
if (Platform.isSupportedTranslationX86_64())
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
}
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
break;
}
@@ -784,13 +795,15 @@ public final class JavaManager {
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) {
if (Platform.isSupportedTranslationX86_64()) {
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);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64()) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
}
@@ -468,7 +468,9 @@ public final class Accounts {
}
} else if (exception instanceof MicrosoftService.XBox400Exception) {
return i18n("account.methods.microsoft.error.wrong_verify_method");
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionLicenseNotFoundException) {
return i18n("account.methods.microsoft.error.no_license");
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionProfileNotFoundException) {
return i18n("account.methods.microsoft.error.no_character");
} else if (exception instanceof MicrosoftService.NoXuiException) {
return i18n("account.methods.microsoft.error.add_family");
@@ -45,7 +45,7 @@ public enum VersionIconType {
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
return switch (modLoaderType) {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGED -> VersionIconType.NEO_FORGE;
case NEO_FORGE -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
@@ -836,7 +836,7 @@ public final class VersionSetting implements Cloneable, Observable {
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;
@@ -909,15 +909,9 @@ public final class VersionSetting implements Cloneable, Observable {
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
.map(Renderer::of).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
}));
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
@@ -927,7 +921,7 @@ public final class VersionSetting implements Cloneable, Observable {
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> vs.getRenderer().getApi()));
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
return vs;
}
@@ -71,9 +71,15 @@ public final class TerracottaBundle {
return new Context() {
@Override
public void withResult(boolean success) {
super.withResult(success);
delegate.withResult(success);
}
@Override
public void reset() throws IOException {
delegate.reset();
}
@Override
public void write(byte[] buffer, int offset, int len) throws IOException {
context.checkCancellation();
@@ -113,7 +119,7 @@ public final class TerracottaBundle {
throw new ArtifactMalformedException(String.format("Expecting %s file in terracotta bundle.", file));
}
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
MessageDigest digest = DigestUtils.getDigest(check.algorithm());
try (
InputStream is = tree.getInputStream(archive);
OutputStream os = new DigestOutputStream(Files.newOutputStream(path), digest)
@@ -122,8 +128,8 @@ public final class TerracottaBundle {
}
String hash = HexFormat.of().formatHex(digest.digest());
if (!check.getChecksum().equalsIgnoreCase(hash)) {
throw new ChecksumMismatchException(check.getAlgorithm(), check.getChecksum(), hash);
if (!check.checksum().equalsIgnoreCase(hash)) {
throw new ChecksumMismatchException(check.algorithm(), check.checksum(), hash);
}
switch (OperatingSystem.CURRENT_OS) {
@@ -172,7 +178,7 @@ public final class TerracottaBundle {
return false;
}
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
MessageDigest digest = DigestUtils.getDigest(check.algorithm());
try (InputStream is = new DigestInputStream(Files.newInputStream(path), digest)) {
int n;
while ((n = is.read(buffer)) >= 0) {
@@ -182,7 +188,7 @@ public final class TerracottaBundle {
}
}
}
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.getChecksum())) {
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.checksum())) {
return false;
}
}
@@ -36,8 +36,9 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.ui.MacOSNativeUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -233,6 +234,11 @@ public final class Themes {
}
});
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) {
MacOSNativeUtils.setAppearance(darkModeProperty().get());
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance);
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
}
}
@@ -70,6 +70,7 @@ import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -213,6 +214,21 @@ public final class Controllers {
return decorator;
}
public static void saveWindowStates() {
if (stageX != null) {
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
}
if (stageY != null) {
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
}
if (stageHeight != null) {
config().setHeight(stageHeight.get());
}
if (stageWidth != null) {
config().setWidth(stageWidth.get());
}
}
public static void onApplicationStop() {
stageSizeChangeListener = null;
if (stageX != null) {
@@ -375,7 +391,7 @@ public final class Controllers {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
continueAction.run();
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
@@ -579,7 +595,7 @@ public final class Controllers {
return pane.getCompletableFuture();
}
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel) {
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, @NotNull TaskCancellationAction onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setExecutor(executor);
@@ -587,7 +603,7 @@ public final class Controllers {
return pane;
}
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, @NotNull TaskCancellationAction onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel);
executor.start();
@@ -62,6 +62,7 @@ import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.CacheFileTask;
import org.jackhuang.hmcl.task.Schedulers;
@@ -1166,12 +1167,10 @@ public final class FXUtils {
}
}
public static Image loadImage(String url) throws Exception {
URI uri = NetworkUtils.toURI(url);
URLConnection connection = NetworkUtils.createConnection(uri);
if (connection instanceof HttpURLConnection)
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
public static Image loadImage(WebURL url) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url);
if (connection instanceof HttpURLConnection httpConnection)
connection = NetworkUtils.resolveConnection(httpConnection);
try (BufferedInputStream input = new BufferedInputStream(connection.getInputStream())) {
String contentType = Objects.requireNonNull(connection.getContentType(), "");
@@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
* @author Glavo
*/
public final class HTMLRenderer {
private static final String INDENT = " ";
private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
@@ -57,6 +58,7 @@ public final class HTMLRenderer {
private boolean underline;
private boolean strike;
private boolean highlight;
private int indentLevel;
private String headerLevel;
private Node hyperlink;
@@ -218,7 +220,7 @@ public final class HTMLRenderer {
appendImage(node);
break;
case "li":
appendText("\n \u2022 ");
appendText("\n" + INDENT.repeat(indentLevel) + " \u2022 ");
break;
case "dt":
appendText(" ");
@@ -237,11 +239,14 @@ public final class HTMLRenderer {
}
if (node.childNodeSize() > 0) {
boolean isLiNode = "li".equals(name);
if (isLiNode) indentLevel++;
pushNode(node);
for (Node childNode : node.childNodes()) {
appendNode(childNode);
}
popNode();
if (isLiNode) indentLevel--;
}
switch (name) {
@@ -30,7 +30,6 @@ import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
@@ -39,7 +38,6 @@ import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
@@ -261,9 +259,6 @@ public final class LogWindow extends Stage {
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final Set<ListCell<Log>> selected = new HashSet<>();
private final JFXSnackbar snackbar = new JFXSnackbar();
LogWindowSkin(LogWindowImpl control) {
@@ -309,6 +304,7 @@ public final class LogWindow extends Stage {
{
ListView<Log> listView = control.listView;
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().addListener((InvalidationListener) observable -> {
if (!listView.getItems().isEmpty() && control.autoScroll.get())
listView.scrollTo(listView.getItems().size() - 1);
@@ -318,7 +314,6 @@ public final class LogWindow extends Stage {
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
listView.setCellFactory(x -> new ListCell<>() {
{
x.setSelectionModel(new NoneMultipleSelectionModel<>());
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
@@ -328,32 +323,6 @@ public final class LogWindow extends Stage {
setPadding(new Insets(2));
setWrapText(true);
setGraphic(null);
setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
if (!event.isControlDown()) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().setSelected(false);
}
}
}
selected.clear();
}
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().setSelected(true);
}
event.consume();
});
}
@Override
@@ -367,7 +336,6 @@ public final class LogWindow extends Stage {
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
if (empty) {
setText(null);
@@ -379,10 +347,13 @@ public final class LogWindow extends Stage {
listView.setOnKeyPressed(event -> {
if (event.isControlDown() && event.getCode() == KeyCode.C) {
if (listView.getSelectionModel().isEmpty())
return;
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.isSelected()) {
for (Log item : listView.getSelectionModel().getSelectedItems()) {
if (item != null) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
@@ -0,0 +1,101 @@
/*
* 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.ui;
import com.sun.jna.Pointer;
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class MacOSNativeUtils {
private static final Pointer nsApp = init();
private static @Nullable Pointer init() {
if (ObjectiveCRuntime.INSTANCE == null) {
return null;
}
try {
var objc = ObjectiveCRuntime.INSTANCE;
Pointer nsApplication = objc.objc_getClass("NSApplication");
if (!isNull(nsApplication)) {
Pointer sharedSel = objc.sel_registerName("sharedApplication");
if (!isNull(sharedSel))
return objc.objc_msgSend(nsApplication, sharedSel);
}
} catch (Throwable e) {
LOG.warning("Failed to initialize macOS appearance support", e);
}
return null;
}
public static boolean isSupported() {
return nsApp != null;
}
private static boolean isNull(Pointer pointer) {
return pointer == null || Pointer.nativeValue(pointer) == 0;
}
public static void setAppearance(boolean dark) {
setAppearance(dark, false);
}
public static void setAppearance(boolean dark, boolean highContrast) {
if (nsApp == null) return;
try {
var objc = ObjectiveCRuntime.INSTANCE;
Pointer nsAppearance = objc.objc_getClass("NSAppearance");
if (isNull(nsAppearance))
return;
Pointer namedSel = objc.sel_registerName("appearanceNamed:");
Pointer nsString = objc.objc_getClass("NSString");
if (isNull(nsString)) return;
Pointer sel = objc.sel_registerName("stringWithUTF8String:");
String appearanceName;
if (highContrast) {
appearanceName = dark ? "NSAppearanceNameAccessibilityHighContrastDarkAqua" : "NSAppearanceNameAccessibilityHighContrastAqua";
} else {
appearanceName = dark ? "NSAppearanceNameDarkAqua" : "NSAppearanceNameAqua";
}
Pointer appearanceNamePtr = objc.objc_msgSend(nsString, sel, appearanceName);
if (isNull(appearanceNamePtr)) return;
Pointer appearance = objc.objc_msgSend(nsAppearance, namedSel, appearanceNamePtr);
if (isNull(appearance)) return;
Pointer setSel = objc.sel_registerName("setAppearance:");
objc.objc_msgSend(nsApp, setSel, appearance);
} catch (Throwable t) {
LOG.warning("Failed to set macOS appearance", t);
}
}
private MacOSNativeUtils() {
}
}
@@ -62,6 +62,7 @@ public enum SVG {
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
FILE_OPEN("M6 22q-0.825 0-1.4125-0.5875T4 20v-16q0-0.825 0.5875-1.4125T6 2h8l6 6v6h-2v-5H13v-5H6v16h9v2H6Zm15.95 0.375L19 19.425v2.225h-2v-5.65h5.65v2h-2.25l2.95 2.95-1.4 1.425Zm-15.95-2.375v-16 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
@@ -50,10 +50,10 @@ public final class UpgradeDialog extends JFXDialogLayout {
setHeading(new Label(i18n("update.changelog")));
setBody(new JFXSpinner());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
String url = CHANGELOG_URL + remoteVersion.channel().channelName + ".html";
Task.supplyAsync(Schedulers.io(), () -> {
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.getVersion());
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.version());
VersionNumber currentVersion = VersionNumber.asVersion(Metadata.VERSION);
if (targetVersion.compareTo(currentVersion) <= 0)
// Downgrade update, no need to display changelog
@@ -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 + "]");
@@ -303,7 +303,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
for (String key : ALLOWED_LINKS) {
String value = links.get(key);
if (value != null) {
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
JFXHyperlink link = new JFXHyperlink(i18n("account.injector.link." + key));
FXUtils.installSlowTooltip(link, value);
link.setOnAction(e -> FXUtils.openLink(value));
result.add(link);
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.geometry.Insets;
@@ -29,6 +30,7 @@ import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
@@ -135,7 +137,9 @@ public class OfflineAccountSkinPane extends StackPane {
capeSelector.setValue(account.getSkin().getLocalCapePath());
}
skinBinding = FXUtils.observeWeak(() -> {
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(1));
Runnable loadSkin = () -> {
getSkin().load(account.getUsername())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
@@ -157,6 +161,23 @@ public class OfflineAccountSkinPane extends StackPane {
result.getCape() != null ? result.getCape().getImage() : null);
}
}).start();
};
pauseTransition.setOnFinished(e -> loadSkin.run());
skinBinding = FXUtils.observeWeak(() -> {
Skin.Type selectedType = skinItem.getSelectedData();
if (selectedType == Skin.Type.CUSTOM_SKIN_LOADER_API) {
if (!cslApiField.validate()) {
pauseTransition.stop();
return;
}
pauseTransition.playFromStart();
} else {
pauseTransition.stop();
loadSkin.run();
}
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -28,6 +30,7 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -43,8 +46,6 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
private boolean expanded = false;
ComponentSublistWrapper(ComponentSublist sublist) {
boolean noPadding = !sublist.hasComponentPadding();
this.getStyleClass().add("options-sublist-wrapper");
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
@@ -104,12 +105,26 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
if (container == null) {
this.container = new VBox();
if (!noPadding) {
container.setPadding(new Insets(8, 16, 10, 16));
}
this.container.getStyleClass().add("container");
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container);
Rectangle rectangle = FXUtils.setOverflowHidden(container);
rectangle.getStyleClass().add("overflow-hidden");
var last = PseudoClass.getPseudoClass("last");
InvalidationListener updateArc = o -> {
if (ComponentSublistWrapper.this.getPseudoClassStates().contains(last)) {
rectangle.setArcHeight(4);
rectangle.setArcWidth(4);
} else {
rectangle.setArcHeight(0);
rectangle.setArcWidth(0);
}
};
updateArc.invalidated(null);
ComponentSublistWrapper.this.getPseudoClassStates().addListener(updateArc);
container.getChildren().setAll(sublist);
ComponentSublistWrapper.this.getChildren().add(container);
@@ -120,8 +135,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
}
Platform.runLater(() -> {
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
double contentHeight = expanded ? sublist.prefHeight(sublist.getWidth()) : 0;
double targetRotate = expanded ? -180 : 0;
if (AnimationUtils.isAnimationEnabled()) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -30,6 +31,7 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Path;
@@ -38,10 +40,18 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FileSelector extends HBox {
private final StringProperty value = new SimpleStringProperty();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private String chooserTitle = "";
private SelectionMode selectionMode = SelectionMode.FILE;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
public enum SelectionMode {
FILE,
DIRECTORY,
FILE_OR_DIRECTORY
}
public String getValue() {
return value.get();
}
@@ -63,12 +73,12 @@ public class FileSelector extends HBox {
return this;
}
public boolean isDirectory() {
return directory;
public SelectionMode getSelectionMode() {
return selectionMode;
}
public FileSelector setDirectory(boolean directory) {
this.directory = directory;
public FileSelector setSelectionMode(SelectionMode selectionMode) {
this.selectionMode = selectionMode;
return this;
}
@@ -80,26 +90,20 @@ public class FileSelector extends HBox {
JFXTextField customField = new JFXTextField();
FXUtils.bindString(customField, valueProperty());
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
selectButton.setOnAction(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
value.setValue(path);
}
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
value.setValue(path);
switch (selectionMode) {
case FILE -> openFileChooser(customField);
case DIRECTORY -> openDirectoryChooser(customField);
case FILE_OR_DIRECTORY -> {
PopupMenu selectPopupMenu = new PopupMenu();
JFXPopup selectModePopup = new JFXPopup(selectPopupMenu);
selectPopupMenu.getContent().addAll(
new IconedMenuItem(SVG.FILE_OPEN, i18n("selector.choose_file"), () -> openFileChooser(customField), selectModePopup),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("selector.choose_directory"), () -> openDirectoryChooser(customField), selectModePopup)
);
selectModePopup.show(selectButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -selectButton.getWidth(), 0);
}
}
});
@@ -108,4 +112,27 @@ public class FileSelector extends HBox {
setSpacing(3);
getChildren().addAll(customField, selectButton);
}
private void openFileChooser(JFXTextField customField) {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
value.setValue(path);
}
}
private void openDirectoryChooser(JFXTextField customField) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
value.setValue(path);
}
}
}
@@ -17,8 +17,14 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
@@ -26,6 +32,10 @@ import javafx.scene.control.ScrollBar;
import javafx.scene.control.Skin;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.util.MathUtils;
// Referenced in root.css
@@ -36,6 +46,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
private Rectangle track = new Rectangle();
private Rectangle thumb = new Rectangle();
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
private ChangeListener<Boolean> thumbHoverListener;
private Animation thumbHoverAnimation;
public FloatScrollBarSkin(final ScrollBar scrollBar) {
this.scrollBar = scrollBar;
scrollBar.setPrefHeight(1e-18);
@@ -45,8 +59,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
Point2D dragStart;
double preDragThumbPos;
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
final NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
final NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
{
// Children are added unmanaged because for some reason the height of the bar keeps changing
@@ -63,7 +77,6 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
setup();
thumb.setOnMousePressed(me -> {
if (me.isSynthesized()) {
// touch-screen events handled by Scroll handler
@@ -128,14 +141,18 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
track.widthProperty().unbind();
track.heightProperty().unbind();
double offset = 4;
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
track.relocate(0, -8);
track.widthProperty().bind(scrollBar.widthProperty());
track.setHeight(8);
track.relocate(offset, -6.0);
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
track.widthProperty().bind(trackWidth);
track.setHeight(6.0);
} else {
track.relocate(-8, 0);
track.setWidth(8);
track.heightProperty().bind(scrollBar.heightProperty());
track.relocate(-6.0, offset);
track.setWidth(6.0);
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
track.heightProperty().bind(trackHeight);
}
thumb.xProperty().unbind();
@@ -144,15 +161,37 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumb.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
thumb.relocate(0, -8);
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
thumb.setHeight(8);
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
thumb.relocate(0, -6.0);
thumb.setHeight(6.0);
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
thumb.widthProperty().bind(Bindings.min(trackWidth,
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackWidth))));
thumb.xProperty().bind(
Bindings.add(offset,
Bindings.multiply(
Bindings.max(0, Bindings.subtract(trackWidth, thumb.widthProperty())),
position)
)
);
} else {
thumb.relocate(-8, 0);
thumb.setWidth(8);
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
thumb.relocate(-6.0, 0);
thumb.setWidth(6.0);
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
thumb.heightProperty().bind(Bindings.min(trackHeight,
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackHeight))));
thumb.yProperty().bind(
Bindings.add(offset,
Bindings.multiply(
Bindings.max(0, Bindings.subtract(trackHeight, thumb.heightProperty())),
position)
)
);
}
}
@@ -162,7 +201,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 8;
return 6.0;
}
@Override
@@ -171,13 +210,46 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 8;
return 6.0;
}
};
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
double targetOpacity = newValue ? 1.0 : 0.5;
double currentOpacity = thumb.getOpacity();
double opacityAdjustment = targetOpacity - currentOpacity;
if (Math.abs(opacityAdjustment) < 0.001) {
if (opacityAdjustment != 0)
thumb.setOpacity(targetOpacity);
return;
}
if (AnimationUtils.isAnimationEnabled() && JFXNodeUtils.isTreeVisible(thumb)) {
thumbHoverAnimation = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(thumb.opacityProperty(), currentOpacity)),
new KeyFrame(Motion.SHORT2, new KeyValue(thumb.opacityProperty(), targetOpacity))
);
thumbHoverAnimation.play();
} else {
thumb.setOpacity(targetOpacity);
}
});
}
@Override
public void dispose() {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
thumbHoverListener = null;
scrollBar = null;
group = null;
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -56,6 +57,7 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
this.setBody(new VBox(textField));
lblCreationWarning = new Label();
lblCreationWarning.setPadding(new Insets(0, 5, 0, 0));
acceptPane = new SpinnerPane();
acceptPane.getStyleClass().add("small-spinner-pane");
@@ -20,10 +20,7 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
@@ -36,6 +33,8 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;
import java.util.Collection;
import java.util.Objects;
@@ -44,7 +43,7 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public class LineSelectButton<T> extends LineButton {
public class LineSelectButton<T extends @UnknownNullability Object> extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
@@ -57,13 +56,9 @@ public class LineSelectButton<T> extends LineButton {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
InvalidationListener updateTrailingText = observable -> {
Function<T, String> converter = getConverter();
T value = getValue();
if (value != null) {
Function<T, String> converter = getConverter();
setTrailingText(converter != null ? converter.apply(value) : value.toString());
} else {
setTrailingText(null);
}
setTrailingText(converter != null ? converter.apply(value) : Objects.toString(value, ""));
};
converterProperty().addListener(updateTrailingText);
valueProperty().addListener(updateTrailingText);
@@ -96,9 +91,6 @@ public class LineSelectButton<T> extends LineButton {
var itemTitleLabel = new Label();
itemTitleLabel.getStyleClass().add("title-label");
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
if (item == null)
return "";
Function<T, String> converter = getConverter();
return converter != null ? converter.apply(item) : Objects.toString(item, "");
}, converterProperty()));
@@ -173,10 +165,14 @@ public class LineSelectButton<T> extends LineButton {
return converterProperty().get();
}
public void setConverter(Function<T, String> value) {
public void setConverter(Function<T, String> value) { // TODO: rename
converterProperty().set(value);
}
public void setNullSafeConverter(Function<@NotNull T, String> value) {
converterProperty().set(it -> it != null ? value.apply(it) : "");
}
private ObjectProperty<Function<T, String>> descriptionConverter;
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
@@ -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);
@@ -55,7 +55,8 @@ public final class MultiFileItem<T> extends VBox {
@SuppressWarnings("unchecked")
public MultiFileItem() {
setPadding(new Insets(0, 0, 10, 0));
this.getStyleClass().add("multi-file-item");
setSpacing(8);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
@@ -266,8 +267,8 @@ public final class MultiFileItem<T> extends VBox {
selector.setValue(value);
}
public FileOption<T> setDirectory(boolean directory) {
selector.setDirectory(directory);
public FileOption<T> setSelectionMode(FileSelector.SelectionMode selectionMode) {
selector.setSelectionMode(selectionMode);
return this;
}
@@ -81,12 +81,11 @@ public class TaskExecutorDialogPane extends BorderPane {
setCancel(cancel);
btnCancel.setDisable(onCancel.getCancellationAction() == null);
btnCancel.setOnAction(e -> {
if (onCancel.getCancellationAction() != null) {
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
}
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
});
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
@@ -30,7 +30,6 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
@@ -147,6 +146,10 @@ public class TwoLineListItem extends VBox {
subtitleProperty().set(subtitle);
}
public HBox getFirstLine() {
return firstLine;
}
public Label getTitleLabel() {
return lblTitle;
}
@@ -168,11 +171,23 @@ 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);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
var expectedHeight = Bindings.createDoubleBinding(
() -> {
if (tags.isEmpty()) return 0.0;
double h = tagsBox.prefHeight(-1);
return h > 0 ? h : Double.NaN;
},
tags, tagsBox.heightProperty()
);
scrollPane.minHeightProperty().bind(expectedHeight);
scrollPane.prefHeightProperty().bind(expectedHeight);
scrollPane.maxHeightProperty().bind(expectedHeight);
firstLine.getChildren().setAll(lblTitle, scrollPane);
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
@@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -54,12 +54,7 @@ public class URLValidator extends ValidatorBase {
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else {
try {
NetworkUtils.toURI(textField.getText());
hasErrors.set(false);
} catch (IllegalArgumentException e) {
hasErrors.set(true);
}
hasErrors.set(WebURL.tryParseBrowserInput(textField.getText()) == null);
}
}
}
@@ -34,6 +34,7 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
@@ -205,7 +206,7 @@ public class DecoratorController {
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) {
try {
image = FXUtils.loadImage(backgroundImageUrl);
image = FXUtils.loadImage(WebURL.parseBrowserInput(backgroundImageUrl));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
@@ -353,12 +353,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
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);
}
}
@@ -171,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), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValidForJar), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}
@@ -423,7 +423,11 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
if (status == Status.LOADING)
transitionPane.setContent(spinner, ContainerAnimations.FADE);
else if (status == Status.SUCCESS)
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
if (control.versions.isEmpty()) {
transitionPane.setContent(emptyPane, ContainerAnimations.FADE);
} else {
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
}
else // if (status == Status.FAILED)
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
});
@@ -214,7 +214,7 @@ public final class ExportWizardProvider implements WizardProvider {
vs.isFullscreen(),
vs.getWidth(),
vs.getHeight(),
vs.getMaxMemory(),
null,
exportInfo.getMinMemory(),
vs.isShowLogs(),
/* showConsoleOnError */ true,
@@ -316,7 +316,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
if (skinnable.options.isRequireAuthlibInjectorServer()) {
var serversSelectButton = new LineSelectButton<AuthlibInjectorServer>();
serversSelectButton.setTitle(i18n("account.injector.server"));
serversSelectButton.setConverter(AuthlibInjectorServer::getName);
serversSelectButton.setNullSafeConverter(AuthlibInjectorServer::getName);
serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl);
serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers());
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.image;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.SnapshotParameters;
@@ -29,6 +28,9 @@ import org.girod.javafx.svgimage.LoaderParameters;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.girod.javafx.svgimage.ScaleQuality;
import org.glavo.webp.WebPImage;
import org.glavo.webp.WebPImageLoadOptions;
import org.glavo.webp.javafx.WebPFXImage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
@@ -38,13 +40,8 @@ import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jackhuang.hmcl.util.SwingFXUtils;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -71,16 +68,8 @@ public final class ImageUtils {
};
public static final ImageLoader WEBP = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
var options = new WebPImageLoadOptions(requestedWidth, requestedHeight, preserveRatio, smooth);
return new WebPFXImage(WebPImage.read(input, options));
};
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
@@ -468,6 +457,19 @@ public final class ImageUtils {
return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
}
private static int[] rgbaToArgb(ByteBuffer rgba) {
int pixelCount = rgba.remaining() / 4;
int[] argb = new int[pixelCount];
for (int i = 0; i < pixelCount; i++) {
int r = rgba.get() & 0xFF;
int g = rgba.get() & 0xFF;
int b = rgba.get() & 0xFF;
int a = rgba.get() & 0xFF;
argb[i] = (a << 24) | (r << 16) | (g << 8) | b;
}
return argb;
}
private ImageUtils() {
}
}
@@ -72,7 +72,7 @@ public final class AboutPage extends SpinnerPane {
ComponentList legal = new ComponentList();
{
var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
var copyright = new LineButton();
copyright.setLargeTitle(true);
copyright.setTitle(i18n("about.copyright"));
copyright.setSubtitle(i18n("about.copyright.statement"));
@@ -27,14 +27,20 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.net.Proxy;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
@@ -72,7 +78,7 @@ public class DownloadSettingsPage extends StackPane {
var versionListSourcePane = new LineSelectButton<String>();
versionListSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty().not());
versionListSourcePane.setTitle(i18n("settings.launcher.version_list_source"));
versionListSourcePane.setConverter(converter);
versionListSourcePane.setNullSafeConverter(converter);
versionListSourcePane.setDescriptionConverter(descriptionConverter);
versionListSourcePane.setItems(DownloadProviders.AUTO_PROVIDERS.keySet());
versionListSourcePane.valueProperty().bindBidirectional(config().versionListSourceProperty());
@@ -80,14 +86,14 @@ public class DownloadSettingsPage extends StackPane {
var downloadSourcePane = new LineSelectButton<String>();
downloadSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty());
downloadSourcePane.setTitle(i18n("settings.launcher.download_source"));
downloadSourcePane.setConverter(converter);
downloadSourcePane.setNullSafeConverter(converter);
downloadSourcePane.setDescriptionConverter(descriptionConverter);
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
var defaultAddonSourcePane = new LineSelectButton<String>();
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
defaultAddonSourcePane.setConverter(key -> I18n.i18n("mods." + key));
defaultAddonSourcePane.setNullSafeConverter(key -> I18n.i18n("mods." + key));
defaultAddonSourcePane.setItems("modrinth", "curseforge");
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
@@ -98,9 +104,37 @@ public class DownloadSettingsPage extends StackPane {
}
{
var downloadList = new ComponentList();
VBox downloadThreads = new VBox(16);
downloadThreads.getStyleClass().add("card-non-transparent");
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
{
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(config().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
@@ -153,7 +187,8 @@ public class DownloadSettingsPage extends StackPane {
}
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadThreads);
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreads);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
}
{
@@ -330,4 +365,11 @@ public class DownloadSettingsPage extends StackPane {
}
}
private void clearCacheDirectory() {
String commonDirectory = Settings.instance().getCommonDirectory();
if (commonDirectory != null) {
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
}
}
}
@@ -28,6 +28,7 @@ import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
@@ -170,6 +171,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.setLimitHeight(updatePane, 55);
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
FXUtils.onClicked(updatePane, this::onUpgrade);
updatePane.setCursor(Cursor.HAND);
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
{
@@ -183,7 +185,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false);
prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion ->
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion())));
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.version())));
hBox.getChildren().setAll(SVG.UPDATE.createIcon(20), prompt);
}
@@ -279,10 +281,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
if (show && !config().isDisableAutoShowUpdateDialog()
&& getLatestVersion() != null
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
&& !Objects.equals(config().getPromptedVersion(), getLatestVersion().version())) {
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().version()), MessageDialogPane.MessageType.INFO)
.addAction(i18n("button.view"), () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
config().setPromptedVersion(getLatestVersion().version());
onUpgrade();
})
.addCancel(null)
@@ -79,7 +79,7 @@ public class PersonalizationPage extends StackPane {
{
var brightnessPane = new LineSelectButton<String>();
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness." + name));
brightnessPane.setNullSafeConverter(name -> i18n("settings.launcher.brightness." + name));
brightnessPane.setItems("auto", "light", "dark");
brightnessPane.valueProperty().bindBidirectional(config().themeBrightnessProperty());
@@ -135,6 +135,7 @@ public class PersonalizationPage extends StackPane {
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
.setChooserTitle(i18n("launcher.background.choose"))
.addExtensionFilter(FXUtils.getImageExtensionFilter())
.setSelectionMode(FileSelector.SelectionMode.FILE_OR_DIRECTORY)
.bindBidirectional(config().backgroundImageProperty()),
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
.setValidators(new URLValidator(true))
@@ -294,7 +295,7 @@ public class PersonalizationPage extends StackPane {
var fontAntiAliasingPane = new LineSelectButton<Optional<FontSmoothingType>>();
fontAntiAliasingPane.setTitle(i18n("settings.launcher.font.anti_aliasing"));
fontAntiAliasingPane.setSubtitle(i18n("settings.take_effect_after_restart"));
fontAntiAliasingPane.setConverter(value ->
fontAntiAliasingPane.setNullSafeConverter(value ->
value.isPresent()
? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT))
: i18n("settings.launcher.font.anti_aliasing.auto")
@@ -20,21 +20,17 @@ package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -61,7 +57,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
@@ -78,242 +76,193 @@ public final class SettingsPage extends ScrollPane {
public SettingsPage() {
this.setFitToWidth(true);
VBox rootPane = new VBox();
VBox rootPane = new VBox(10);
rootPane.setPadding(new Insets(10));
this.setContent(rootPane);
FXUtils.smoothScrolling(this);
ComponentList settingsPane = new ComponentList();
{
ComponentList updatePaneList = new ComponentList();
{
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
FXUtils.onClicked(sponsorPane, this::onSponsor);
sponsorPane.setPadding(new Insets(8, 0, 8, 0));
GridPane gridPane = new GridPane();
ColumnConstraints col = new ColumnConstraints();
col.setHgrow(Priority.SOMETIMES);
col.setMaxWidth(Double.POSITIVE_INFINITY);
gridPane.getColumnConstraints().setAll(col);
RowConstraints row = new RowConstraints();
row.setMinHeight(Double.NEGATIVE_INFINITY);
row.setValignment(VPos.TOP);
row.setVgrow(Priority.SOMETIMES);
gridPane.getRowConstraints().setAll(row);
ObjectProperty<UpdateChannel> updateChannel;
{
Label label = new Label(i18n("sponsor.hmcl"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
sponsorPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(sponsorPane);
}
JFXButton updateButton = FXUtils.newToggleButton4(SVG.UPDATE, 20);
updateButton.setOnAction(e -> onUpdate());
updateButton.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
ObjectProperty<UpdateChannel> updateChannel;
{
var updatePane = new LineSelectButton<UpdateChannel>() {
JFXButton updateButton = new JFXButton();
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
updateButton.setOnAction(e -> onUpdate());
updateButton.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
{
getStyleClass().add("update-pane");
setNode(IDX_TRAILING, updateButton);
}
var updatePane = new LineSelectButton<UpdateChannel>() {
{
getStyleClass().add("update-pane");
setNode(IDX_TRAILING, updateButton);
}
@Override
protected int getTrailingTextIndex() {
return LineComponent.IDX_TRAILING + 1;
}
};
updateChannel = updatePane.valueProperty();
updatePane.setTitle(i18n("update"));
updatePane.setValue(UpdateChannel.getChannel());
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
{
updateListener = any -> {
boolean outdated = UpdateChecker.isOutdated();
updateButton.setVisible(outdated);
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
if (UpdateChecker.isOutdated()) {
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSubProperty.set(i18n("update.checking"));
} else {
lblUpdateSubProperty.set(i18n("update.latest"));
@Override
protected int getTrailingTextIndex() {
return LineComponent.IDX_TRAILING + 1;
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
updateChannel = updatePane.valueProperty();
updatePane.setTitle(i18n("update"));
updatePane.setValue(UpdateChannel.getChannel());
updatePane.setNullSafeConverter(channel -> i18n("update.channel." + channel.channelName));
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
{
updateListener = any -> {
boolean outdated = UpdateChecker.isOutdated();
updateButton.setVisible(outdated);
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
if (UpdateChecker.isOutdated()) {
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSubProperty.set(i18n("update.checking"));
} else {
lblUpdateSubProperty.set(i18n("update.latest"));
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
}
updatePaneList.getContent().add(updatePane);
}
settingsPane.getContent().add(updatePane);
{
LineToggleButton previewPane = new LineToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
updatePaneList.getContent().add(previewPane);
}
{
LineToggleButton disableAutoShowUpdateDialogPane = new LineToggleButton();
disableAutoShowUpdateDialogPane.setTitle(i18n("update.disable_auto_show_update_dialog"));
disableAutoShowUpdateDialogPane.setSubtitle(i18n("update.disable_auto_show_update_dialog.subtitle"));
disableAutoShowUpdateDialogPane.selectedProperty().bindBidirectional(config().disableAutoShowUpdateDialogProperty());
updatePaneList.getContent().add(disableAutoShowUpdateDialogPane);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("update")), updatePaneList);
}
{
LineToggleButton previewPane = new LineToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
ComponentList languagePaneList = new ComponentList();
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
{
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
settingsPane.getContent().add(previewPane);
SupportedLocale currentLocale = I18n.getLocale();
chooseLanguagePane.setNullSafeConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
});
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
languagePaneList.getContent().add(chooseLanguagePane);
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
languagePaneList.getContent().add(disableAutoGameOptionsPane);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.language")), languagePaneList);
}
{
LineToggleButton disableAutoShowUpdateDialogPane = new LineToggleButton();
disableAutoShowUpdateDialogPane.setTitle(i18n("update.disable_auto_show_update_dialog"));
disableAutoShowUpdateDialogPane.setSubtitle(i18n("update.disable_auto_show_update_dialog.subtitle"));
disableAutoShowUpdateDialogPane.selectedProperty().bindBidirectional(config().disableAutoShowUpdateDialogProperty());
settingsPane.getContent().add(disableAutoShowUpdateDialogPane);
ComponentList miscPaneList = new ComponentList();
if (AprilFools.isShowAprilFoolsSettings()) {
LineToggleButton disableAprilFools = new LineToggleButton();
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
miscPaneList.getContent().add(disableAprilFools);
}
{
LineToggleButton allowAutoAgentPane = new LineToggleButton();
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
miscPaneList.getContent().add(allowAutoAgentPane);
}
{
BorderPane debugPane = new BorderPane();
Label left = new Label(i18n("settings.launcher.debug"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
SpinnerPane exportLogPane = new SpinnerPane();
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
miscPaneList.getContent().add(debugPane);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.misc")), miscPaneList);
}
if (AprilFools.isShowAprilFoolsSettings()) {
LineToggleButton disableAprilFools = new LineToggleButton();
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
settingsPane.getContent().add(disableAprilFools);
}
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setDirectory(true)
.bindBidirectional(config().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
settingsPane.getContent().add(fileCommonLocationSublist);
}
{
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
SupportedLocale currentLocale = I18n.getLocale();
chooseLanguagePane.setConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
});
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
settingsPane.getContent().add(chooseLanguagePane);
}
{
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
LineToggleButton allowAutoAgentPane = new LineToggleButton();
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
settingsPane.getContent().add(allowAutoAgentPane);
}
{
BorderPane debugPane = new BorderPane();
Label left = new Label(i18n("settings.launcher.debug"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
SpinnerPane exportLogPane = new SpinnerPane();
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
settingsPane.getContent().add(debugPane);
}
rootPane.getChildren().add(settingsPane);
}
}
@@ -452,15 +401,4 @@ public final class SettingsPage extends ScrollPane {
return outputFile;
}), Schedulers.io());
}
private void onSponsor() {
FXUtils.openLink("https://github.com/HMCL-dev/HMCL");
}
private void clearCacheDirectory() {
String commonDirectory = Settings.instance().getCommonDirectory();
if (commonDirectory != null) {
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
}
}
}
@@ -58,14 +58,14 @@ public class SkinCube extends MeshView {
5, 0, 4, 1, 0, 5, //P5,T0, P4,T1, P0,T5
5, 0, 0, 5, 1, 4, //P5,T0, P0,T5, P1,T4
// RIGHT
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
5, 3, 1, 4, 3, 9, //P5,T3, P1,T4, P3,T9
5, 3, 3, 9, 7, 8, //P5,T3, P3,T9, P7,T8
// FRONT
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
// LEFT
5, 3, 1, 4, 3, 9, //P5,T6, P1,T5, P3,T10
5, 3, 3, 9, 7, 8, //P5,T6, P3,T10,P7,T11
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
// BACK
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
@@ -131,14 +131,15 @@ public class SkinMultipleCubes extends Group {
// LEFT
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y + depth_interval, depth, height, interval, false, false,
() -> new Box(thick, length, length), (b, p) -> {
b.setTranslateX((width * length + thick) / 2.0);
b.setTranslateX(-(width * length + thick) / 2.0);
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
}));
// RIGHT
getChildren().add(new Face(skin, start_x, start_y + depth_interval, depth, height, interval, true, false,
() -> new Box(thick, length, length), (b, p) -> {
b.setTranslateX(-(width * length + thick) / 2.0);
b.setTranslateX((width * length + thick) / 2.0);
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
}));
@@ -246,7 +246,7 @@ public class TerracottaControllerPage extends StackPane {
} else {
handler.resolve();
}
});
}, "", new RequiredValidator(i18n("input.not_empty")));
});
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
@@ -183,18 +183,15 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
nativesDirSublist.setHasSubtitle(true);
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.advanced.natives_directory.custom"), NativesDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
.setDirectory(true);
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
nativesDirItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
nativesDirCustomOption
));
HintPane nativesDirHint = new HintPane(MessageDialogPane.MessageType.WARNING);
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.setNullSafeConverter(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");
@@ -213,28 +210,26 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
rendererPane.setNullSafeConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
rendererPane.setDescriptionConverter(e -> {
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
rendererPane.setValue(Renderer.DEFAULT);
rendererPane.setItems(Renderer.DEFAULT);
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
if (backend == null) { // unbind
return;
}
rendererPane.setItems(Renderer.SUPPORTED.get(backend));
rendererPane.setItems(Renderer.getSupported(backend));
if (backend == GraphicsAPI.DEFAULT) {
rendererPane.setDisable(true);
rendererPane.setValue(Renderer.DEFAULT);
} else {
rendererPane.setDisable(false);
if (rendererPane.getValue() == null || !rendererPane.getValue().isSupported(backend)) {
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
rendererPane.setValue(Renderer.DEFAULT);
}
}
});
@@ -28,6 +28,7 @@ import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -97,6 +98,14 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
listView = new JFXListView<>();
filteredList = new FilteredList<>(skinnable.getItems());
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
var selectAllButton = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
ListChangeListener<Object> listener = change -> {
selectAllButton.setDisable(!listView.getItems().isEmpty()
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
};
{
toolbarPane = new TransitionPane();
searchBar = new HBox();
@@ -123,12 +132,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
enableButton.disableProperty().bind(getSkinnable().readOnly);
disableButton.disableProperty().bind(getSkinnable().readOnly);
listView.getSelectionModel().getSelectedItems().addListener(listener);
selectingToolbar.getChildren().addAll(
removeButton,
enableButton,
disableButton,
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
selectAllButton,
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
@@ -184,6 +194,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
this.listView.setItems(filteredList);
listView.getItems().addListener(listener);
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
@@ -531,7 +531,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
listView.setCellFactory(x -> new ListCell<>() {
private static final Insets PADDING = new Insets(9, 9, 0, 9);
@@ -578,7 +577,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
} else {
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(item.getSlug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.getTitle());
content.setSubtitle(item.getDescription());
String description = item.getDescription();
if (description != null) {
description = description.replaceAll("\\R", " ");
}
content.setSubtitle(description);
content.getTags().clear();
for (String category : item.getCategories()) {
if (getSkinnable().shouldDisplayCategory(category))
@@ -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) {
@@ -439,7 +441,7 @@ public class DownloadPage extends Control implements DecoratorPage {
case CLEANROOM:
content.addTag(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
case NEO_FORGE:
content.addTag(i18n("install.installer.neoforge"));
break;
case FABRIC:
@@ -160,11 +160,11 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
supportedLoaders.add(ModLoaderType.FORGE);
supportedLoaders.add(ModLoaderType.NEO_FORGED);
supportedLoaders.add(ModLoaderType.NEO_FORGE);
}
// Sinytra Connector
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED)
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
@@ -54,7 +54,10 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -154,6 +157,18 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
);
// Toolbar Selecting
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
ListChangeListener<Object> listener = change -> {
selectAll.setDisable(!listView.getItems().isEmpty()
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
};
listView.getSelectionModel().getSelectedItems().addListener(listener);
listView.getItems().addListener(listener);
toolbarSelecting.getChildren().setAll(
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
@@ -171,8 +186,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
.toList()
)
),
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectAll()),
selectAll,
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
@@ -473,7 +487,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
case FORGE -> i18n("install.installer.forge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
case NEO_FORGED -> i18n("install.installer.neoforge");
case NEO_FORGE -> i18n("install.installer.neoforge");
case FABRIC -> i18n("install.installer.fabric");
case LITE_LOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
@@ -643,7 +657,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
case NEO_FORGED -> content.addTagWarning(i18n("install.installer.neoforge"));
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
@@ -239,7 +239,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.disableProperty().bind(modpack);
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.game.working_directory.choose"))
.setDirectory(true);
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
gameDirItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("settings.advanced.game_dir.default"), GameDirectoryType.ROOT_FOLDER),
@@ -336,7 +336,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
launcherVisibilityPane = new LineSelectButton<>();
launcherVisibilityPane.setTitle(i18n("settings.advanced.launcher_visible"));
launcherVisibilityPane.setItems(LauncherVisibility.values());
launcherVisibilityPane.setConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
launcherVisibilityPane.setNullSafeConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
BorderPane dimensionPane = new BorderPane();
{
@@ -351,7 +351,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
cboWindowsSize.setPrefWidth(150);
right.setLeft(cboWindowsSize);
cboWindowsSize.setEditable(true);
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
cboWindowsSize.setPromptText("854x480");
cboWindowsSize.getItems().setAll(getSupportedResolutions());
@@ -373,7 +372,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
enableDebugLogOutputPane.setTitle(i18n("settings.enable_debug_log_output"));
processPriorityPane = new LineSelectButton<>();
processPriorityPane.setTitle(i18n("settings.advanced.process_priority"));
processPriorityPane.setConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
processPriorityPane.setNullSafeConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
processPriorityPane.setDescriptionConverter(e -> {
String bundleKey = "settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
@@ -264,6 +264,11 @@ public final class Versions {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
if (file != null) {
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
String defaultExt = getDefaultScriptExtension();
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
}
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
@@ -273,6 +278,21 @@ public final class Versions {
});
}
private static boolean isValidScriptExtension(String ext) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
return ext.equalsIgnoreCase("bat") || ext.equalsIgnoreCase("ps1");
}
return ext.equalsIgnoreCase("sh") || ext.equalsIgnoreCase("bash") || ext.equalsIgnoreCase("command") || ext.equalsIgnoreCase("ps1");
}
private static String getDefaultScriptExtension() {
return switch (OperatingSystem.CURRENT_OS) {
case WINDOWS -> "bat";
case MACOS -> "command";
default -> "sh";
};
}
@SafeVarargs
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id))
@@ -40,10 +40,12 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.NotNull;
import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@@ -153,7 +155,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
LOG.warning("Failed to create backup", exception);
Controllers.dialog(i18n("world.backup.create.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING);
}
}), i18n("world.backup"), null);
}), i18n("world.backup"), TaskCancellationAction.NO_CANCEL);
}
private final class WorldBackupsPageSkin extends ToolbarListPageSkin<BackupInfo, WorldBackupsPage> {
@@ -24,13 +24,11 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.glavo.nbt.tag.*;
@@ -162,11 +160,10 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
seedPane.setTitle(i18n("world.info.random_seed"));
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
StackPane visibilityButton = new StackPane();
JFXButton visibilityButton = FXUtils.newToggleButton4(SVG.VISIBILITY_OFF);
{
visibilityButton.setCursor(Cursor.HAND);
visibilityButton.setAlignment(Pos.CENTER_RIGHT);
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
visibilityButton.setAlignment(Pos.CENTER);
visibilityButton.setOnAction((event) -> visibility.set(!visibility.get()));
}
Label seedLabel = new Label();
@@ -180,7 +177,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
blur.setIterations(3);
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
visibilityButton.getChildren().setAll(icon.createIcon(12));
visibilityButton.setGraphic(icon.createIcon(16));
seedLabel.setEffect(isVisibility ? null : blur);
});
}
@@ -27,8 +27,8 @@ final class HMCLDownloadTask extends FileDownloadTask {
private final RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(version.getUrl(), target, version.getIntegrityCheck());
archiveFormat = version.getType();
super(version.url(), target, version.integrityCheck());
archiveFormat = version.type();
}
@Override
@@ -23,11 +23,13 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Optional;
public final class RemoteVersion {
public record RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck,
boolean preview, boolean force) {
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
try {
@@ -46,54 +48,8 @@ public final class RemoteVersion {
}
}
private final UpdateChannel channel;
private final String version;
private final String url;
private final Type type;
private final IntegrityCheck integrityCheck;
private final boolean preview;
private final boolean force;
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
this.channel = channel;
this.version = version;
this.url = url;
this.type = type;
this.integrityCheck = integrityCheck;
this.preview = preview;
this.force = force;
}
public UpdateChannel getChannel() {
return channel;
}
public String getVersion() {
return version;
}
public String getUrl() {
return url;
}
public Type getType() {
return type;
}
public IntegrityCheck getIntegrityCheck() {
return integrityCheck;
}
public boolean isPreview() {
return preview;
}
public boolean isForce() {
return force;
}
@Override
public String toString() {
public @NotNull String toString() {
return "[" + version + " from " + url + "]";
}
@@ -43,13 +43,13 @@ public final class UpdateChecker {
RemoteVersion latest = latestVersion.get();
if (latest == null || isDevelopmentVersion(Metadata.VERSION)) {
return false;
} else if (latest.isForce()
} else if (latest.force()
|| Metadata.isNightly()
|| latest.getChannel() == UpdateChannel.NIGHTLY
|| latest.getChannel() != UpdateChannel.getChannel()) {
return !latest.getVersion().equals(Metadata.VERSION);
|| latest.channel() == UpdateChannel.NIGHTLY
|| latest.channel() != UpdateChannel.getChannel()) {
return !latest.version().equals(Metadata.VERSION);
} else {
return VersionNumber.compare(Metadata.VERSION, latest.getVersion()) < 0;
return VersionNumber.compare(Metadata.VERSION, latest.version()) < 0;
}
},
latestVersion);
@@ -20,20 +20,20 @@ package org.jackhuang.hmcl.upgrade;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import javafx.application.Platform;
import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
@@ -43,13 +43,15 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class UpdateHandler {
private UpdateHandler() {
@@ -125,6 +127,29 @@ public final class UpdateHandler {
throw new IOException("Current JAR is not verified");
}
CompletableFuture<Void> future = new CompletableFuture<>();
Platform.runLater(() -> {
try {
Controllers.saveWindowStates();
} finally {
future.complete(null);
}
});
try {
future.get();
} catch (ExecutionException | InterruptedException ignored) {
// Ignore
}
try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
// Ignore
}
requestUpdate(downloaded, getCurrentLocation());
EntryPoint.exit(0);
} catch (IOException e) {
@@ -197,7 +197,7 @@ public final class NativePatcher {
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
return getNatives(java.getPlatform()).get("mesa-loader");
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
if (renderer == Renderer.LLVMPIPE)
if (renderer == Renderer.OpenGL.LLVMPIPE)
return getNatives(java.getPlatform()).get("software-renderer-loader");
else
return null;
@@ -1,162 +0,0 @@
// Copy from javafx.swing
/*
* Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.jackhuang.hmcl.util;
import javafx.scene.image.*;
import javafx.scene.image.Image;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.nio.IntBuffer;
/**
* This class provides utility methods for converting data types between
* Swing/AWT and JavaFX formats.
*
* @since JavaFX 2.2
*/
public final class SwingFXUtils {
private SwingFXUtils() {
}
/**
* Snapshots the specified {@link BufferedImage} and stores a copy of
* its pixels into a JavaFX {@link Image} object, creating a new
* object if needed.
* The returned {@code Image} will be a static snapshot of the state
* of the pixels in the {@code BufferedImage} at the time the method
* completes. Further changes to the {@code BufferedImage} will not
* be reflected in the {@code Image}.
* <p>
* The optional JavaFX {@link WritableImage} parameter may be reused
* to store the copy of the pixels.
* A new {@code Image} will be created if the supplied object is null,
* is too small or of a type which the image pixels cannot be easily
* converted into.
*
* @param bimg the {@code BufferedImage} object to be converted
* @param wimg an optional {@code WritableImage} object that can be
* used to store the returned pixel data
* @return an {@code Image} object representing a snapshot of the
* current pixels in the {@code BufferedImage}.
* @since JavaFX 2.2
*/
public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) {
int bw = bimg.getWidth();
int bh = bimg.getHeight();
switch (bimg.getType()) {
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
break;
default:
BufferedImage converted =
new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE);
Graphics2D g2d = converted.createGraphics();
g2d.drawImage(bimg, 0, 0, null);
g2d.dispose();
bimg = converted;
break;
}
// assert(bimg.getType == TYPE_INT_ARGB[_PRE]);
if (wimg != null) {
int iw = (int) wimg.getWidth();
int ih = (int) wimg.getHeight();
if (iw < bw || ih < bh) {
wimg = null;
} else if (bw < iw || bh < ih) {
int[] empty = new int[iw];
PixelWriter pw = wimg.getPixelWriter();
PixelFormat<IntBuffer> pf = PixelFormat.getIntArgbPreInstance();
if (bw < iw) {
pw.setPixels(bw, 0, iw - bw, bh, pf, empty, 0, 0);
}
if (bh < ih) {
pw.setPixels(0, bh, iw, ih - bh, pf, empty, 0, 0);
}
}
}
if (wimg == null) {
wimg = new WritableImage(bw, bh);
}
PixelWriter pw = wimg.getPixelWriter();
DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer();
int[] data = db.getData();
int offset = bimg.getRaster().getDataBuffer().getOffset();
int scan = 0;
SampleModel sm = bimg.getRaster().getSampleModel();
if (sm instanceof SinglePixelPackedSampleModel) {
scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride();
}
PixelFormat<IntBuffer> pf = (bimg.isAlphaPremultiplied() ?
PixelFormat.getIntArgbPreInstance() :
PixelFormat.getIntArgbInstance());
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan);
return wimg;
}
public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return toFXImage(bimg, null);
}
int width = (int) requestedWidth;
int height = (int) requestedHeight;
// Calculate actual dimensions if preserveRatio is true
if (preserveRatio) {
double originalWidth = bimg.getWidth();
double originalHeight = bimg.getHeight();
double scaleX = requestedWidth / originalWidth;
double scaleY = requestedHeight / originalHeight;
double scale = Math.min(scaleX, scaleY);
width = (int) (originalWidth * scale);
height = (int) (originalHeight * scale);
}
// Create scaled BufferedImage
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
Graphics2D g2d = scaledImage.createGraphics();
try {
if (smooth) {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g2d.drawImage(bimg, 0, 0, width, height, null);
} finally {
g2d.dispose();
}
return toFXImage(scaledImage, null);
}
}
@@ -93,5 +93,15 @@
"title": "LWJGL Unsafe Agent",
"subtitle": "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink": "https://github.com/HMCL-dev/lwjgl-unsafe-agent"
},
{
"title": "TomlJ",
"subtitle": "Licensed under the Apache 2.0 License.",
"externalLink": "https://github.com/tomlj/tomlj"
},
{
"title": "WebURL for Java",
"subtitle": "Licensed under the Apache 2.0 License.",
"externalLink": "https://github.com/Glavo/weburl-java"
}
]
@@ -70,6 +70,12 @@
"subtitleLocalized" : "about.thanks_to.mcim.statement",
"externalLink" : "https://github.com/mcmod-info-mirror"
},
{
"image" : "/assets/img/8mi-tech.png",
"titleLocalized" : "about.thanks_to.8mi-tech",
"subtitleLocalized" : "about.thanks_to.8mi-tech.statement",
"externalLink" : "https://8mi.tech"
},
{
"image" : {
"light" : "/assets/img/github.png",
+20 -4
View File
@@ -29,8 +29,8 @@
.scroll-bar .thumb {
-fx-fill: -monet-surface-tint;
-fx-arc-width: 8;
-fx-arc-height: 8;
-fx-arc-width: 6;
-fx-arc-height: 6;
}
.title-label {
@@ -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 {
@@ -1093,8 +1093,12 @@
-fx-background-color: -monet-error-container;
}
.multi-file-item {
-fx-padding: 10 0 10 0;
}
.options-sublist {
-fx-background-color: -monet-surface;
-fx-background-color: -monet-surface-container;
}
.options-list-item {
@@ -1105,6 +1109,14 @@
-fx-font-size: 12;
}
.options-sublist .options-list-item {
-fx-background-color: -monet-surface-container;
}
.options-sublist .multi-file-item {
-fx-padding: 10 10 10 10;
}
.options-list-item .svg {
-fx-fill: -monet-on-surface;
-fx-border-color: -monet-outline-variant;
@@ -1140,6 +1152,10 @@
-fx-border-width: 0;
}
.options-sublist-wrapper > .container {
-fx-background-color: -monet-surface-container;
}
.options-sublist-wrapper .expand-icon .svg {
-fx-fill: -monet-on-surface-variant;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -48,6 +48,8 @@ about.thanks_to.shulkersakura.statement=For providing the logo for HMCL.
about.thanks_to.users=HMCL User Group Members
about.thanks_to.users.statement=Thanks for donations, bug reports, and so on.
about.thanks_to.yushijinhun.statement=For providing the authlib-injector related support.
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
about.thanks_to.8mi-tech.statement=Provides an accelerated download service for some of the game's static resources.
about.open_source=Open Source
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -108,9 +110,9 @@ account.methods.microsoft.close_page=Microsoft account authorization is now comp
account.methods.microsoft.deauthorize=Deauthorize
account.methods.microsoft.error.add_family=Please click <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">here</a> to change your account birth date to be over 18 years old, or add your account to a family.
account.methods.microsoft.error.country_unavailable=Xbox Live is not available in your current country/region.
account.methods.microsoft.error.missing_xbox_account=Your Microsoft account does not have a linked Xbox account yet. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to link one.
account.methods.microsoft.error.no_character=Please confirm that you have purchased Minecraft: Java Edition.\n\
If you have already purchased it, a game profile may not have been created. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create a game profile.
account.methods.microsoft.error.missing_xbox_account=Please confirm that you have purchased Minecraft: Java Edition. If you have, please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create your game profile.
account.methods.microsoft.error.no_license=No Minecraft purchase record found. Please click <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">here</a> to get the game.\nIf you are using Game Pass, please create your game profile <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a>. If you have already purchased the game, please check if you are logged into the correct account.
account.methods.microsoft.error.no_character=You have not created a game profile yet. Please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create one.
account.methods.microsoft.error.banned=Your account may have been banned by Xbox Live.\n\
You can click <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">here</a> to check the ban status of your account.
account.methods.microsoft.error.unknown=Failed to log in, error code: %d.
@@ -363,16 +365,16 @@ download.failed.empty=No versions are available. Please click here to go back.
download.failed.no_code=Failed to download
download.failed.refresh=Failed to fetch version list. Please click here to retry.
download.game=New Game
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=This option is provided exclusively for users in Chinese Mainland.
download.provider.mojang=Official
download.provider.mojang.desc=OptiFine is provided by BMCLAPI
download.provider.mojang.desc=Note that OptiFine is provided by BMCLAPI.
download.provider.official=From Official Sources
download.provider.official.desc=Latest, but may load slowly
download.provider.official.desc=Always obtain from official sources.
download.provider.balanced=From Fastest Available
download.provider.balanced.desc=Balanced, but may not be the latest
download.provider.balanced.desc=Best choice for ALL users.
download.provider.mirror=From Mirror
download.provider.mirror.desc=Fast, but may not be the latest
download.provider.mirror.desc=Recommended option for users in Chinese Mainland.
download.java=Downloading Java
download.java.process=Java Download Process
download.javafx=Downloading dependencies for the launcher...
@@ -452,8 +454,6 @@ fatal.unsupported_platform=Minecraft is not fully supported on your platform yet
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher has provided support for the Apple silicon platform, using native ARM Java to launch games to get a smoother gaming experience.\n\
If you encounter problems when playing a game, launching the game with Java based on x86-64 architecture may offer better compatibility.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
\n\
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
@@ -1309,6 +1309,7 @@ search.page_n=%1$d / %2$s
selector.choose=Choose
selector.choose_file=Choose file
selector.choose_directory=Choose directory
selector.custom=Custom
settings=Settings
@@ -1365,10 +1366,6 @@ settings.advanced.natives_directory.choose=Choose the location of the desired na
settings.advanced.natives_directory.custom=Custom
settings.advanced.natives_directory.default=Default
settings.advanced.natives_directory.default.version_id=<Instance Name>
settings.advanced.natives_directory.hint=This option is intended only for users of Apple silicon or other not officially supported platforms. Please do not edit this option unless you know what you are doing.\n\
\n\
Before proceeding, please make sure all libraries (e.g. lwjgl.dll, libopenal.so) are provided in your desired directory.\n\
Note: It is recommended to use a fully English-letters path for the specified local library file. Otherwise it may lead to game launch failure.
settings.advanced.no_jvm_args=Do not add default JVM arguments
settings.advanced.no_optimizing_jvm_args=Do not add default JVM optimization arguments
settings.advanced.precall_command=Pre-launch Command
@@ -1389,14 +1386,44 @@ settings.advanced.post_exit_command.prompt=Commands to execute after the game ex
settings.advanced.renderer=Renderer
settings.advanced.renderer.default=Default
settings.advanced.renderer.default.desc=Use System Default
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software OpenGL 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 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=OpenGL renderer based on Vulkan
settings.advanced.server_ip=Server Address
@@ -1457,6 +1484,7 @@ settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.default_addon_source=Default Addon Source
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.misc=Misc
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
settings.launcher.font.anti_aliasing.auto=Auto
@@ -354,18 +354,11 @@ download.external_link=زيارة موقع التنزيل
download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d.
download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع.
download.failed.no_code=فشل التنزيل
download.failed.refresh=فشل جلب قائمة الإصدارات. انقر هنا لإعادة المحاولة.
download.game=لعبة جديدة
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=رسمي
download.provider.mojang.desc=يتم توفير OptiFine عبر BMCLAPI
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
download.game=New Game
download.provider.official=من المصادر الرسمية
download.provider.official.desc=الأحدث، لكن قد يكون التحميل بطيئاً
download.provider.balanced=من أسرع المصادر المتاحة
download.provider.balanced.desc=متوازن، لكن قد لا يكون الأحدث
download.provider.mirror=من مرآة
download.provider.mirror.desc=سريع، لكن قد لا يكون الأحدث
download.java=جارٍ تنزيل Java
download.java.process=عملية تنزيل Java
download.javafx=جارٍ تنزيل مكتبات المشغّل...
@@ -445,8 +438,6 @@ fatal.unsupported_platform=لا يدعم Minecraft منصتك الحالية ب
إذا لم تتمكن من تشغيل Minecraft 1.17 أو أحدث، يمكنك محاولة تغيير "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام معالجة المعالج المركزي لتوافق أفضل.
fatal.unsupported_platform.loongarch=وفّر Hello Minecraft! Launcher دعماً لمنصة Loongson.\n\
إذا واجهت مشاكل أثناء اللعب، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على مساعدة.
fatal.unsupported_platform.macos_arm64=وفّر Hello Minecraft! Launcher دعماً لمنصة Apple silicon، باستخدام Java ARM الأصلي لتشغيل الألعاب للحصول على تجربة لعب أكثر سلاسة.\n\
إذا واجهت مشاكل أثناء اللعب، قد يوفر تشغيل اللعبة بـ Java المبني على معمارية x86-64 توافقاً أفضل.
fatal.unsupported_platform.windows_arm64=وفّر Hello Minecraft! Launcher دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
\n\
إذا كنت تستخدم منصة <b>Qualcomm</b>، قد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل اللعب.\n\
@@ -1341,10 +1332,6 @@ settings.advanced.natives_directory.choose=اختر موقع المكتبة ال
settings.advanced.natives_directory.custom=مخصص
settings.advanced.natives_directory.default=افتراضي
settings.advanced.natives_directory.default.version_id=<اسم النسخة>
settings.advanced.natives_directory.hint=هذا الخيار مخصص فقط لمستخدمي Apple silicon والمنصات غير المدعومة رسمياً. يُرجى عدم تعديل هذا الخيار إلا إذا كنت تعرف ما تفعله.\n\
\n\
قبل المتابعة، يُرجى التأكد من توفر جميع المكتبات (مثل lwjgl.dll وlibopenal.so) في المجلد المحدد.\n\
ملاحظة: يُنصح باستخدام مسار يحتوي على حروف إنجليزية فقط لملف المكتبة المحلية المحدد، وإلا قد يتسبب ذلك في فشل تشغيل اللعبة.
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
settings.advanced.precall_command=أمر ما قبل التشغيل
@@ -336,10 +336,6 @@ download.failed.empty=No hay versiones disponibles, por favor haga clic aquí pa
download.failed.no_code=No se ha podido descargar
download.failed.refresh=No se ha podido obtener la lista de versiones. Por favor, haga clic aquí para volver a intentarlo.
download.game=Nuevo juego
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Oficial
download.provider.mojang.desc=OptiFine es proporcionado por BMCLAPI
download.provider.official=De fuentes oficiales
download.provider.balanced=De la fuente más rápida disponible
download.provider.mirror=Desde espejo
@@ -418,8 +414,6 @@ fatal.unsupported_platform=Minecraft aún no es totalmente compatible con tu pla
Si no puedes iniciar Minecraft 1.17 y versiones posteriores, puedes intentar cambiar el «Renderizador» a «Mesa LLVMpipe» en «Configuración global/específica de la instancia → Configuración avanzada» para utilizar el renderizado por CPU y mejorar la compatibilidad.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher ha prestado apoyo a la plataforma Loongson.\n\
Si tienes problemas al jugar, puedes visitar https://docs.hmcl.net/groups.html para obtener ayuda.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher ha proporcionado soporte para la plataforma de chips de Apple, utilizando Java nativo de ARM para ejecutar juegos y conseguir una experiencia de juego más fluida.\n\
Si tienes problemas al jugar a un juego, ejecutarlo con Java de arquitectura x86-64 puede tener mejor compatibilidad.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher ha proporcionado soporte nativo para la plataforma Windows en Arm. Si tiene problemas al jugar a un juego, intente iniciarlo con Java de arquitectura x86.\n\
\n\
Si utilizas la plataforma <b>Qualcomm</b>, es posible que tengas que instalar el <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">paquete de compatibilidad OpenGL</a> antes de jugar.\n\
@@ -1251,9 +1245,6 @@ settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca
settings.advanced.natives_directory.custom=Personalizado
settings.advanced.natives_directory.default=Por defecto
settings.advanced.natives_directory.default.version_id=<Nombre de la instancia>
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de chips de Apple u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
\n\
Antes de proceder, por favor, asegúrese de que todas las bibliotecas (por ejemplo, lwjgl.dll, libopenal.so) se proporcionan en su directorio deseado.
settings.advanced.no_jvm_args=No añadir argumentos JVM por defecto
settings.advanced.no_optimizing_jvm_args=No añadir argumentos de optimización de JVM
settings.advanced.precall_command=Comando pre-lanzamiento:
@@ -293,10 +293,6 @@ download.failed=%1$s のダウンロードに失敗しました、応答コー
download.failed.empty=候補者なし。戻るにはここをクリックしてください。
download.failed.refresh=バージョンリストをダウンロードできません。ここをクリックして再試行してください。
download.game=ゲームのダウンロード
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=ミラーソース
download.provider.mojang=Mojang
download.provider.mojang.desc=OptiFineはBMCLAPIにより提供されます
download.provider.official=公式ソースからロード
download.provider.balanced=mcbbsのソースからロード
download.provider.mirror=ミラーソースからロード
@@ -778,7 +774,6 @@ settings.advanced.natives_directory=ネイティブライブラリパス(例
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
settings.advanced.natives_directory.hint=このオプションは、Appleシリコンまたはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。HMCLは、不足しているネイティブライブラリを提供しません。
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
settings.advanced.precall_command=起動前コマンド
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
@@ -46,6 +46,8 @@ about.thanks_to.shulkersakura.statement=鑄其徽
about.thanks_to.users=HMCL 群組諸員
about.thanks_to.users.statement=敬謝諸君之貲勉、報謬、謀案
about.thanks_to.yushijinhun.statement=有助於 authlib-injector
about.thanks_to.8mi-tech=八藍米資訊科技 (泉州) 有限公所
about.thanks_to.8mi-tech.statement=供遊戲部分靜態資源之加速下載役
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -332,10 +334,6 @@ download.failed.empty=[無可裝之版,點此返]\n(君可求助於右上之
download.failed.no_code=引之未成
download.failed.refresh=[載版列未成,點此重試]\n(君可求助於右上之鈕)
download.game=新戲
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93,https://bmclapi2.bangbang93.com
download.provider.mojang=官版
download.provider.mojang.desc=Optifine 猶取諸 BMCLAPI 也
download.provider.official=先官版
download.provider.official.desc=至新,容有艱
download.provider.balanced=先其快者
@@ -385,7 +383,6 @@ fatal.illegal_char=君之案夾名有諱文「=」,是以外載之戶簿、離
fatal.unsupported_platform=礦藝未盡適君之算機,是以戲事或損,至於不能啟。\n\
礦藝一點一七以晉之不能啟,或於「(全例/例殊) 戲設 → 進階置設 → 勘誤置設」改「繪製之器」以「Mesa LLVMpipe」,或能兼之。\n君可求助於右上之鈕。
fatal.unsupported_platform.loongarch=HMCL 既適龍芯。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.macos_arm64=HMCL 既適蘋矽。宜啟以 ARM 之爪哇,以益君之戲事。\n誠有謬,宜啟以 x86-64 之爪哇,以益其兼。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.windows_arm64=HMCL 既適 ARM 之視窗。誠有謬,宜啟以 x86 之爪哇。\n誠用<b>栝柑</b>,或須置<a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">開圖庫兼囊</a>,而後可戯。擊鏈以置之於微軟貨舍。\n君可求助於右上之鈕。
file=案
@@ -1066,7 +1063,6 @@ settings.advanced.natives_directory.choose=擇府庫之徑
settings.advanced.natives_directory.custom=自定 (賴君所供之府庫也)
settings.advanced.natives_directory.default=本 (賴啟者所供之府庫)
settings.advanced.natives_directory.default.version_id=<例名>
settings.advanced.natives_directory.hint=供於械網之弗見適者,有如蘋矽,以自定府庫也。微其之知,毋改是,改則或弗啟。\n\n將改,而宜錄府庫於是,蓋 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 之類。啟者弗補是府庫也!\n\n誡:府庫之徑,但宜書以英文、數、下綫,否則或弗啟。
settings.advanced.no_jvm_args=無添爪哇虛機之本通弦
settings.advanced.precall_command=令於戲前
settings.advanced.precall_command.prompt=將前戲啟而行
@@ -333,10 +333,6 @@ download.failed.empty=Версий нет. Нажмите здесь, чтобы
download.failed.no_code=Не удалось скачать
download.failed.refresh=Не удалось получить список версий. Нажмите здесь, чтобы повторить попытку.
download.game=Новая игра
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Официальный
download.provider.mojang.desc=OptiFine предоставляется BMCLAPI
download.provider.official=Из официальных источников
download.provider.balanced=Из самых быстрых доступных
download.provider.mirror=Из зеркала
@@ -412,8 +408,6 @@ fatal.unsupported_platform=Minecraft еще не полностью поддер
Если вы не можете запустить Minecraft версии 1.17 и новее, попробуйте переключить «Рендерер» на «Mesa LLVMpipe» в разделе «Глобальные/настройки экземпляра → Расширенные настройки», чтобы использовать рендеринг через CPU для лучшей совместимости.
fatal.unsupported_platform.loongarch=Лаунчер обеспечил поддержку платформы Loongson.\n\
Если у вас возникнут проблемы во время игры, вы можете обратиться за помощью на сайт https://docs.hmcl.net/groups.html.
fatal.unsupported_platform.macos_arm64=Лаунчер обеспечил поддержку платформы Apple silicon, используя родную ARM Java для запуска игр, чтобы получить более плавный игровой опыт.\n\
Если вы столкнулись с проблемами во время игры, запуск игры с Java на базе архитектуры x86-64 может обеспечить лучшую совместимость.
fatal.unsupported_platform.windows_arm64=Лаунчер обеспечил нативную поддержку платформы Windows на архитектуре Arm. Если у вам возникли проблемы во время игры, попробуйте запустить игру с Java на базе архитектуры x86.\n\
\n\
Если вы используете платформу <b>Qualcomm</b>, вам может потребоваться установить <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">пакет совместимости OpenGL</a> перед началом игры.\n\
@@ -1242,10 +1236,6 @@ settings.advanced.natives_directory.choose=Выберите расоложени
settings.advanced.natives_directory.custom=Пользовательское
settings.advanced.natives_directory.default=По умолчанию
settings.advanced.natives_directory.default.version_id=<Название сборки>
settings.advanced.natives_directory.hint=Параметр предназначен только для пользователей Apple silicon или других официально не поддерживаемых платформ. Не изменяйте, если не знаете что делаете.\n\
\n\
Прежде чем продолжить, убедитесь что все библиотеки (например, lwjgl.dll, libopenal.so) находятся в нужной папке.\n\
Примечание: Рекомендуется использовать полностью английские пути для указанных файлов локальных библиотек, иначе игра может не запуститься.
settings.advanced.no_jvm_args=Не добавлять аргументы JVM по умолчанию
settings.advanced.precall_command=Команда перед запуском
settings.advanced.precall_command.prompt=Команды, которые необходимо выполнить перед запуском игры
@@ -332,10 +332,6 @@ download.failed.empty=Немає доступних версій. Натисні
download.failed.no_code=Не вдалося завантажити
download.failed.refresh=Не вдалося отримати список версій. Натисніть тут, щоб повторити спробу.
download.game=Нова гра
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Офіційне
download.provider.mojang.desc=OptiFine надається BMCLAPI
download.provider.official=З офіційних джерел
download.provider.balanced=З найшвидшого доступного
download.provider.mirror=З дзеркала
@@ -398,8 +394,6 @@ fatal.unsupported_platform=Minecraft ще не повністю підтриму
Якщо ви не можете запустити Minecraft версії 1.17 і новіші, спробуйте змінити «Рендерер» на «Mesa LLVMpipe» у розділі «Глобальні/налаштування екземпляра → Розширені налаштування», щоб використовувати рендеринг через CPU для кращої сумісності.
fatal.unsupported_platform.loongarch=Hello Minecraft! Лаунчер надав підтримку платформи Loongson.\n\
Якщо у вас виникли проблеми під час гри, ви можете відвідати https://docs.hmcl.net/groups.html для отримання допомоги.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Лаунчер надав підтримку платформи Apple silicon, використовуючи нативну ARM Java для запуску ігор для отримання плавнішого ігрового досвіду.\n\
Якщо у вас виникли проблеми під час гри, запуск гри з Java на основі архітектури x86-64 може забезпечити кращу сумісність.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Лаунчер надав нативну підтримку платформи Windows on Arm. Якщо у вас виникли проблеми під час гри, спробуйте запустити гру з Java на основі архітектури x86.\n\
Якщо ви використовуєте платформу <b>Qualcomm</b>, вам може знадобитися встановити <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">Пакет сумісності OpenGL</a> перед грою.\n\
Натисніть посилання, щоб перейти до Microsoft Store та встановити пакет сумісності.
@@ -1190,9 +1184,6 @@ settings.advanced.natives_directory.choose=Виберіть місце розт
settings.advanced.natives_directory.custom=Власний
settings.advanced.natives_directory.default=Типово
settings.advanced.natives_directory.default.version_id=<Назва екземпляра>
settings.advanced.natives_directory.hint=Ця опція призначена лише для користувачів Apple silicon або інших неофіційно підтримуваних платформ. Не редагуйте цю опцію, якщо ви не знаєте, що робите. \n\
Перед продовженням переконайтеся, що всі бібліотеки (наприклад, lwjgl.dll, libopenal.so) надано у вашому бажаному каталозі. \n\
Примітка: Рекомендується використовувати повністю англійські літери для вказаного локального файлу бібліотеки. Інакше це може призвести до невдалого запуску гри.
settings.advanced.no_jvm_args=Не додавати типові аргументи JVM
settings.advanced.precall_command=Команда перед запуском
settings.advanced.precall_command.prompt=Команди для виконання перед запуском гри
@@ -47,6 +47,8 @@ about.thanks_to.shulkersakura.statement=提供 HMCL 的標誌
about.thanks_to.users=HMCL 使用者群組成員
about.thanks_to.users.statement=感謝使用者群組成員贊助充電、積極催更、回報問題、出謀劃策
about.thanks_to.yushijinhun.statement=提供 authlib-injector 相關支援
about.thanks_to.8mi-tech=八藍米信息科技 (泉州) 有限公司
about.thanks_to.8mi-tech.statement=提供了遊戲部分靜態資源的加速下載服務
about.open_source=開放原始碼
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL/)
@@ -109,8 +111,9 @@ account.methods.microsoft.close_page=已完成 Microsoft 帳戶授權。啟動
account.methods.microsoft.deauthorize=移除應用存取權
account.methods.microsoft.error.add_family=請點擊 <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">此處</a> 更改你的帳戶出生日期,使年齡滿 18 歲以上,或將帳戶加入到家庭中。
account.methods.microsoft.error.country_unavailable=你所在的國家或地區不受 Xbox Live 的支援。
account.methods.microsoft.error.missing_xbox_account=請點擊下方 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 關聯 Xbox 帳戶。
account.methods.microsoft.error.no_character=請確認你已經購買了 Minecraft: Java 版。\n若已購買,則可能未建立遊戲檔案。請點擊 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。
account.methods.microsoft.error.missing_xbox_account=請確認你已經購買了 Minecraft: Java 版。若已購買,請點擊 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。
account.methods.microsoft.error.no_license=未找到 Minecraft 購買記錄。請點擊 <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">此處</a> 購買遊戲。\n若你使用 Game Pass,請在 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。若已購買,請檢查登入的帳戶是否正確。
account.methods.microsoft.error.no_character=你可能未建立遊戲檔案。請點擊 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此處</a> 建立遊戲檔案。
account.methods.microsoft.error.banned=你的帳戶可能被 Xbox Live 封禁。\n你可以點擊 <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">此處</a> 按鈕查詢帳戶封禁狀態。
account.methods.microsoft.error.unknown=登入失敗。錯誤碼:%d。
account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Microsoft 帳戶登入頁面嘗試使用密碼登入,不要使用其他登入方式。
@@ -359,16 +362,16 @@ download.failed.empty=沒有可供安裝的版本,按一下此處返回。
download.failed.no_code=下載失敗
download.failed.refresh=載入版本清單失敗,按一下此處重試。
download.game=新遊戲
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=適用於中國大陸的鏡像源
download.provider.mojang=官方伺服器
download.provider.mojang.desc=OptiFine 由 BMCLAPI 提供
download.provider.official=盡量使用官方源
download.provider.official.desc=最新,但可能載入慢
download.provider.balanced=選取載入速度快的下載源
download.provider.balanced.desc=平衡,但可能不是最新
download.provider.mojang.desc=注意:OptiFine 的下載由 BMCLAPI 提供
download.provider.official=盡量使用官方來源
download.provider.official.desc=優先從官方來源獲取資訊
download.provider.balanced=選取載入速度快的下載來源
download.provider.balanced.desc=根據你的網路連線狀況選用最佳來源
download.provider.mirror=盡量使用鏡像源
download.provider.mirror.desc=載入快,但可能不是最新
download.provider.mirror.desc=推薦中國大陸使用者使用
download.java=下載 Java
download.java.process=下載 Java
download.javafx=正在下載必要的執行時元件
@@ -419,7 +422,6 @@ fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
@@ -1099,6 +1101,7 @@ search.page_n=%d / %s
selector.choose=選取
selector.choose_file=選取檔案
selector.choose_directory=選取目錄
selector.custom=自訂
settings=設定
@@ -1155,7 +1158,6 @@ settings.advanced.natives_directory.choose=選取本機庫路徑
settings.advanced.natives_directory.custom=自訂 (由你提供遊戲需要的本機庫)
settings.advanced.natives_directory.default=預設 (由啟動器提供遊戲本機庫)
settings.advanced.natives_directory.default.version_id=<實例名稱>
settings.advanced.natives_directory.hint=本選項提供給 Apple Silicon 等未受遊戲官方支援的平台來自訂遊戲本機庫。如果你不知道本選項的含義,請你不要修改本選項,否則會導致遊戲無法啟動。\n\n如果你要修改本選項,你需要保證自訂目錄下有遊戲所需的本機庫檔案,如 lwjgl.dll (liblwjgl.so), openal.dll (libopenal.so) 等檔案。啟動器不會幫你補全缺少的本機庫檔案。\n\n注意:建議指定的本機庫檔案路徑使用全英文字元,否則可能導致遊戲啟動失敗。
settings.advanced.no_jvm_args=不新增預設的 Java 虛擬機參數
settings.advanced.no_optimizing_jvm_args=不自動新增 Java 虛擬機最佳化參數
settings.advanced.precall_command=遊戲啟動前執行指令
@@ -1173,17 +1175,47 @@ 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=使用系統預設繪製器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基於 DirectX 12 的 OpenGL 繪製器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=軟體 Vulkan 繪製器
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟體 OpenGL 繪製器
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=軟體 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 的 OpenGL 繪製器
settings.advanced.server_ip=伺服器位址
@@ -1242,6 +1274,7 @@ settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.default_addon_source=遊戲內容預設下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.misc=雜項
settings.launcher.font=字體
settings.launcher.font.anti_aliasing=反鋸齒
settings.launcher.font.anti_aliasing.auto=自動
@@ -47,6 +47,8 @@ about.thanks_to.shulkersakura.statement=提供 HMCL 的徽标
about.thanks_to.users=HMCL 用户群成员
about.thanks_to.users.statement=感谢用户群成员赞助充电、积极催更、反馈问题、出谋划策
about.thanks_to.yushijinhun.statement=提供 authlib-injector 相关支持
about.thanks_to.8mi-tech=八蓝米信息科技 (泉州) 有限公司
about.thanks_to.8mi-tech.statement=提供了游戏部分静态资源的加速下载服务
about.open_source=开源
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -110,8 +112,9 @@ account.methods.microsoft.close_page=已完成微软账户授权。其余登录
account.methods.microsoft.deauthorize=解除账户授权
account.methods.microsoft.error.add_family=请点击 <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">此处</a> 更改你的账户出生日期,使年龄满 18 岁以上,或将账户加入到家庭中。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.country_unavailable=你所在的国家或地区不受 Xbox Live 的支持。
account.methods.microsoft.error.missing_xbox_account=请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 关联 Xbox 账户。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.no_character=请确认你已经购买了 Minecraft: Java 版。\n若已购买,则可能未创建游戏档案。请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.missing_xbox_account=请确认你已经购买了 Minecraft: Java 版。若已购买,请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.no_license=未找到 Minecraft 购买记录。请点击 <a href="https://www.xbox.com/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj">此处</a> 购买游戏。\n若你使用 Game Pass,请在 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。若已购买,请检查登录的账户是否正确。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.no_character=你尚未创建游戏档案。请点击 <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">此处</a> 创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.banned=你的账户可能被 Xbox Live 封禁。\n你可以点击 <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">此处</a> 按钮查询账户封禁状态。
account.methods.microsoft.error.unknown=未知问题。错误码:%d。\n你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软账户登录页面使用密码登录,不要使用其他登录方式。\n你可以点击右上角帮助按钮进行求助。
@@ -361,16 +364,16 @@ download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你
download.failed.no_code=下载失败
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
download.game=新游戏
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=适用于中国大陆的镜像源
download.provider.mojang=官方
download.provider.mojang.desc=OptiFine 自动安装使用 BMCLAPI 下载源
download.provider.mojang.desc=注:OptiFine 自动安装使用 BMCLAPI 下载源
download.provider.official=尽量使用官方源
download.provider.official.desc=最新,但可能加载慢
download.provider.official.desc=优先从官方源获取信息
download.provider.balanced=选择加载速度快的下载源
download.provider.balanced.desc=平衡,但可能不是最新
download.provider.balanced.desc=根据你的网络连接情况选择最佳源
download.provider.mirror=尽量使用镜像源
download.provider.mirror.desc=加载快,但可能不是最新
download.provider.mirror.desc=推荐中国大陆用户使用
download.java=下载 Java
download.java.process=下载 Java
download.javafx=正在下载必要的运行时组件……
@@ -423,7 +426,6 @@ fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
@@ -1016,7 +1018,7 @@ world.info.player.game_type=游戏模式
world.info.player.game_type.adventure=冒险
world.info.player.game_type.creative=创造
world.info.player.game_type.hardcore=极限
world.info.player.game_type.spectator=旁观者
world.info.player.game_type.spectator=旁观
world.info.player.game_type.survival=生存
world.info.player.health=生命值
world.info.player.last_death_location=上次死亡位置
@@ -1104,6 +1106,7 @@ search.page_n=%d / %s
selector.choose=选择
selector.choose_file=选择文件
selector.choose_directory=选择文件夹
selector.custom=自定义
settings=设置
@@ -1160,7 +1163,6 @@ settings.advanced.natives_directory.choose=选择本地库路径
settings.advanced.natives_directory.custom=自定义 (由你提供游戏需要的本地库)
settings.advanced.natives_directory.default=默认 (由启动器提供游戏本地库)
settings.advanced.natives_directory.default.version_id=<实例名称>
settings.advanced.natives_directory.hint=本选项提供给 Apple Silicon 等未受游戏官方支持的平台来自定义游戏本地库。如果你不知道本选项的含义,请不要修改本选项,否则会导致游戏无法启动!\n\n如果你要修改本选项,你需要保证自定义文件夹下有游戏所需的本地库文件,如 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 等文件。启动器不会帮你补全缺少的本地库文件!\n\n注意:指定的本地库文件路径建议只包含英文大小写字母、数字和下划线,否则可能会导致启动游戏失败。
settings.advanced.no_jvm_args=不添加默认的 Java 虚拟机参数
settings.advanced.no_optimizing_jvm_args=不自动添加 Java 虚拟机优化参数
settings.advanced.precall_command=游戏启动前执行命令
@@ -1178,17 +1180,47 @@ 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=使用系统默认渲染器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基于 DirectX 12 的 OpenGL 渲染器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基于 DirectX 12 的 Vulkan 渲染器 (实验性)
# 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=软件 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 的 OpenGL 渲染器
settings.advanced.server_ip=服务器地址
@@ -1247,6 +1279,7 @@ settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.default_addon_source=游戏内容默认下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.misc=杂项
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿
settings.launcher.font.anti_aliasing.auto=自动
@@ -44,6 +44,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
}
.dialog-card {
border-radius: 4px;
background-color: var(--monet-surface-container-high);
padding: 24px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
+2 -1
View File
@@ -16,7 +16,7 @@ dependencies {
api(libs.kala.compress.tar)
api(libs.simple.png.javafx)
api(libs.gson)
api(libs.toml)
api(libs.tomlj)
api(libs.xz)
api(libs.lz4)
api(libs.fx.gson)
@@ -27,6 +27,7 @@ dependencies {
api(libs.jna)
api(libs.pci.ids)
api(libs.hello.nbt)
api(libs.weburl)
compileOnlyApi(libs.jetbrains.annotations)
@@ -24,12 +24,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -58,13 +57,17 @@ public class AuthlibInjectorServer implements Observable {
public static AuthlibInjectorServer locateServer(String url) throws IOException {
try {
url = NetworkUtils.addHttpsIfMissing(url);
HttpURLConnection conn = NetworkUtils.createHttpConnection(url);
WebURL webURL = WebURL.parseBrowserInput(url);
url = webURL.toString();
HttpURLConnection conn = NetworkUtils.createHttpConnection(webURL);
conn = NetworkUtils.resolveConnection(conn);
String ali = conn.getHeaderField("x-authlib-injector-api-location");
if (ali != null) {
URI absoluteAli = conn.getURL().toURI().resolve(NetworkUtils.toURI(ali));
if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) {
WebURL absoluteAli = WebURL.parse(ali, WebURL.of(conn.getURL()));
if (!urlEqualsIgnoreSlash(webURL.toString(), absoluteAli.toString())) {
conn.disconnect();
url = absoluteAli.toString();
conn = NetworkUtils.resolveConnection(NetworkUtils.createHttpConnection(absoluteAli));
@@ -81,7 +84,7 @@ public class AuthlibInjectorServer implements Observable {
} finally {
conn.disconnect();
}
} catch (IllegalArgumentException | URISyntaxException e) {
} catch (IllegalArgumentException e) {
throw new IOException(e);
}
}
@@ -25,7 +25,10 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.*;
@@ -245,7 +248,16 @@ public class MicrosoftService {
.createConnection();
int responseCode = conn.getResponseCode();
if (responseCode == HTTP_NOT_FOUND) {
throw new NoMinecraftJavaEditionProfileException();
MinecraftLicense license = HttpRequest.GET("https://api.minecraftservices.com/entitlements/license")
.authorization(tokenType, accessToken)
.getJson(MinecraftLicense.class);
boolean hasMinecraftLicense = license != null && license.items() != null && license.items().stream()
.anyMatch(item -> "game_minecraft".equals(item.name()));
if (!hasMinecraftLicense) {
throw new MinecraftJavaEditionLicenseNotFoundException();
} else {
throw new MinecraftJavaEditionProfileNotFoundException();
}
} else if (responseCode != 200) {
throw new ResponseCodeException("https://api.minecraftservices.com/minecraft/profile", responseCode);
}
@@ -324,7 +336,10 @@ public class MicrosoftService {
public final static class XBox400Exception extends AuthenticationException {
}
public final static class NoMinecraftJavaEditionProfileException extends AuthenticationException {
public final static class MinecraftJavaEditionProfileNotFoundException extends AuthenticationException {
}
public final static class MinecraftJavaEditionLicenseNotFoundException extends AuthenticationException {
}
public final static class NoXuiException extends AuthenticationException {
@@ -425,6 +440,21 @@ public class MicrosoftService {
}
@JsonSerializable
public record MinecraftLicense(
@SerializedName("items") List<MinecraftLicenseItem> items,
@SerializedName("signature") String signature,
@SerializedName("keyId") String keyId
) {
}
@JsonSerializable
public record MinecraftLicenseItem(
@SerializedName("name") String name,
@SerializedName("signature") String signature
) {
}
public static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation {
@SerializedName("id")
UUID id;
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.UrlResponseInfo;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream;
@@ -244,10 +245,15 @@ public class Skin {
}
@Override
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
protected Context getContext(@Nullable HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
return new Context() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@Override
public void reset() throws IOException {
baos.reset();
}
@Override
public void write(byte[] buffer, int offset, int len) {
baos.write(buffer, offset, len);
@@ -260,7 +266,7 @@ public class Skin {
setResult(new ByteArrayInputStream(baos.toByteArray()));
if (checkETag) {
repository.cacheBytes(response, baos.toByteArray());
repository.cacheBytes(UrlResponseInfo.of(response), baos.toByteArray());
}
}
};
@@ -242,7 +242,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
}
},
CLEANROOM(true, "cleanroom", "com\\.cleanroommc", "cleanroom", ModLoaderType.CLEANROOM),
NEO_FORGE(true, "neoforge", "net\\.neoforged\\.fancymodloader", "(core|loader)", ModLoaderType.NEO_FORGED) {
NEO_FORGE(true, "neoforge", "net\\.neoforged\\.fancymodloader", "(core|loader)", ModLoaderType.NEO_FORGE) {
private final Pattern NEO_FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
@Override
@@ -390,7 +390,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher";
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
public static final String FORGE_BOOTSTRAP_MAIN = "net.minecraftforge.bootstrap.ForgeBootstrap";
public static final String NEO_FORGED_BOOTSTRAP_MAIN = "net.neoforged.fml.startup.Client";
public static final String NEO_FORGE_BOOTSTRAP_MAIN = "net.neoforged.fml.startup.Client";
public static final Set<String> FORGE_OPTIFINE_MAIN = Set.of(
LibraryAnalyzer.VANILLA_MAIN,
@@ -398,7 +398,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
LibraryAnalyzer.MOD_LAUNCHER_MAIN,
LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN,
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN,
LibraryAnalyzer.NEO_FORGED_BOOTSTRAP_MAIN
LibraryAnalyzer.NEO_FORGE_BOOTSTRAP_MAIN
);
public static final VersionRange<VersionNumber> FORGE_OPTIFINE_BROKEN_RANGE = VersionNumber.between("48.0.0", "49.0.50");
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ZlibUtils;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
@@ -168,6 +169,22 @@ public class ForgeNewInstallTask extends Task<Version> {
}
if (!Objects.equals(code, entry.getValue())) {
if (!ZlibUtils.IS_ZLIB_COMPATIBLE && FileUtils.getExtension(artifact).equals("jar")) {
// Forge/NeoForge generates JARs dynamically during installation.
// When native compression libraries such as zlib-ng are in use,
// the resulting JAR may be compressed differently, causing its
// SHA-1 hash to differ from the expected value recorded in the
// install profile. In this case, fall back to verifying that the
// file is at least a structurally valid ZIP/JAR archive.
try {
FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER.checkIntegrity(artifact, artifact);
LOG.info("Ignoring SHA-1 mismatch for " + artifact + " due to non-standard zlib compression output");
continue;
} catch (Exception ignored) {
}
}
Files.delete(artifact);
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
}
@@ -52,10 +52,13 @@ public final class LegacyFabricVersionList extends VersionList<LegacyFabricRemot
lock.writeLock().lock();
try {
for (String gameVersion : gameVersions)
for (String loaderVersion : loaderVersions)
for (String metaGameVersion : gameVersions) {
String gameVersion = normalizeVersion(metaGameVersion);
for (String loaderVersion : loaderVersions) {
versions.put(gameVersion, new LegacyFabricRemoteVersion(gameVersion, loaderVersion,
Collections.singletonList(getLaunchMetaUrl(gameVersion, loaderVersion))));
Collections.singletonList(getLaunchMetaUrl(metaGameVersion, loaderVersion))));
}
}
} finally {
lock.writeLock().unlock();
}
@@ -71,6 +74,12 @@ public final class LegacyFabricVersionList extends VersionList<LegacyFabricRemot
.stream().map(GameVersion::getVersion).collect(Collectors.toList());
}
private static String normalizeVersion(String version) {
return version.startsWith("2point0_")
? "2.0_" + version.substring("2point0_".length())
: version;
}
private static String getLaunchMetaUrl(String gameVersion, String loaderVersion) {
return String.format("https://meta.legacyfabric.net/v2/versions/loader/%s/%s", gameVersion, loaderVersion);
}
@@ -17,73 +17,529 @@
*/
package org.jackhuang.hmcl.game;
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.util.EnumMap;
import java.util.List;
import java.util.Map;
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 enum Renderer {
DEFAULT(GraphicsAPI.DEFAULT, null, null),
public sealed interface Renderer permits Renderer.Default, Renderer.Driver, Renderer.Unknown {
LAVAPIPE(GraphicsAPI.VULKAN, "lavapipe", "lvp"),
/// Default renderer.
Default DEFAULT = new Default();
// 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(GraphicsAPI.VULKAN, "dzn", "dzn"),
/// Parse a renderer from a string.
static Renderer of(String name) {
if ("DEFAULT".equalsIgnoreCase(name) || name.isBlank())
return DEFAULT;
LLVMPIPE(GraphicsAPI.OPENGL, "llvmpipe", null),
ZINK(GraphicsAPI.OPENGL, "zink", null),
D3D12(GraphicsAPI.OPENGL, "d3d12", null),
;
String upper = name.toUpperCase(Locale.ROOT).trim();
/// Map the graphics API to supported renderers.
public static final Map<GraphicsAPI, List<Renderer>> SUPPORTED;
static {
var map = new EnumMap<GraphicsAPI, List<Renderer>>(GraphicsAPI.class);
for (var api : GraphicsAPI.values()) {
map.put(api, Stream.of(values()).filter(it -> it.isSupported(api)).toList());
try {
return OpenGL.valueOf(upper);
} catch (IllegalArgumentException ignored) {
}
SUPPORTED = map;
try {
return Vulkan.valueOf(upper);
} catch (IllegalArgumentException ignored) {
}
return new Unknown(name);
}
private final GraphicsAPI api;
private final @Nullable String loaderName;
private final @Nullable String icdName;
Renderer(GraphicsAPI api, @Nullable String loaderName, @Nullable String icdName) {
this.api = api;
this.loaderName = loaderName;
this.icdName = icdName;
/// 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;
};
}
/// Get the Graphics API used by this renderer.
public GraphicsAPI getApi() {
return api;
String name();
final class Default implements Renderer {
private Default() {
}
@Override
public String name() {
return "DEFAULT";
}
}
public boolean isSupported(GraphicsAPI api) {
return this.api == api || this.api == GraphicsAPI.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;
}
}
public @Nullable String getMesaDriverName() {
return loaderName;
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);
}
}
public @Nullable String getIcdName() {
return icdName;
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;
}
}
public @Nullable String getIcdFileName() {
return icdName != null ? icdName + "_icd.json" : null;
/// 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;
}
}
@@ -88,13 +88,15 @@ public final class World {
root = fs.getPath("/");
fileName = FileUtils.getName(this.file);
} else {
List<Path> files = Files.list(fs.getPath("/")).toList();
if (files.size() != 1 || !Files.isDirectory(files.get(0))) {
throw new IOException("Not a valid world zip file");
}
try (Stream<Path> filesStream = Files.list(fs.getPath("/"))) {
List<Path> files = filesStream.toList();
if (files.size() != 1 || !Files.isDirectory(files.get(0))) {
throw new IOException("Not a valid world zip file");
}
root = files.get(0);
fileName = FileUtils.getName(root);
root = files.get(0);
fileName = FileUtils.getName(root);
}
}
Path levelDat = root.resolve("level.dat");
@@ -27,6 +27,7 @@ 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;
@@ -108,7 +109,7 @@ public class DefaultLauncher extends Launcher {
res.add(options.getJava().getBinary().toString());
res.addAllWithoutParsing(options.getOverrideJavaArguments());
res.addAllWithoutParsingAndReadExternal(options.getOverrideJavaArguments());
if (options.getMaxMemory() != null && options.getMaxMemory() > 0)
res.addDefault("-Xmx", options.getMaxMemory() + "m");
@@ -266,11 +267,20 @@ public class DefaultLauncher extends Launcher {
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& options.getRenderer() != null
&& options.getRenderer().getMesaDriverName() != null) {
&& 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)) {
@@ -602,28 +612,6 @@ public class DefaultLauncher extends Launcher {
return p;
}
private @Nullable Path findVulkanDescriptorFile(List<Path> dirs, String driverNameBase) {
String archName = switch (options.getJava().getArchitecture()) {
case X86 -> "i686";
case X86_64 -> "x86_64";
default -> options.getJava().getArchitecture().getCheckedName();
};
for (Path dir : dirs) {
if (Files.isDirectory(dir)) {
Path file = dir.resolve(driverNameBase + "." + archName + ".json");
if (Files.isRegularFile(file))
return file;
file = dir.resolve(driverNameBase + ".json");
if (Files.isRegularFile(file))
return file;
}
}
return null;
}
private Map<String, String> getEnvVars(Path nativeFolder) {
String versionName = Optional.ofNullable(options.getVersionName()).orElse(version.getId());
@@ -634,28 +622,31 @@ 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.getMesaDriverName() != null) {
if (renderer.getApi() == GraphicsAPI.OPENGL && renderer != Renderer.LLVMPIPE)
env.put("GALLIUM_DRIVER", renderer.getMesaDriverName());
if (renderer.getApi() == GraphicsAPI.VULKAN) {
String icdFile = FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader/" + renderer.getIcdFileName()));
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 == OperatingSystem.LINUX) {
switch (renderer) {
case LLVMPIPE: {
} 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");
/*
@@ -665,21 +656,24 @@ public class DefaultLauncher extends Launcher {
* Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093
*/
env.put("LIBGL_KOPPER_DRI2", "1");
break;
}
case LAVAPIPE: {
Path lvp = findVulkanDescriptorFile(
List.of(Path.of("/usr/share/vulkan/icd.d"), Path.of("/etc/vulkan/icd.d")),
"lvp_icd"
);
if (lvp != null) {
env.put("VK_ICD_FILENAMES", FileUtils.getAbsolutePath(lvp));
env.put("VK_DRIVER_FILES", FileUtils.getAbsolutePath(lvp));
}
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);
}
}
}
@@ -728,10 +722,10 @@ public class DefaultLauncher extends Launcher {
boolean usePowerShell = "ps1".equals(scriptExtension);
if (!usePowerShell) {
if (isWindows && !scriptExtension.equals("bat"))
if (isWindows && !scriptExtension.equalsIgnoreCase("bat"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' or 'ps1' in Windows");
else if (!isWindows && !(scriptExtension.equals("sh") || scriptExtension.equals("command")))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh', 'ps1' or 'command' in macOS/Linux");
else if (!isWindows && !(scriptExtension.equalsIgnoreCase("sh") || scriptExtension.equalsIgnoreCase("command") || scriptExtension.equalsIgnoreCase("bash")))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh', 'bash', 'ps1' or 'command' in macOS/Linux");
}
final Command commandLine = generateCommandLine(nativeFolder);
@@ -21,7 +21,7 @@ public enum ModLoaderType {
UNKNOWN,
FORGE,
CLEANROOM,
NEO_FORGED,
NEO_FORGE,
FABRIC,
QUILT,
LITE_LOADER,
@@ -47,7 +47,7 @@ public final class ModManager {
var map = new HashMap<String, List<Pair<ModMetadataReader, ModLoaderType>>>();
var zipReaders = List.<Pair<ModMetadataReader, ModLoaderType>>of(
pair(ForgeNewModMetadata::fromForgeFile, ModLoaderType.FORGE),
pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGED),
pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGE),
pair(ForgeOldModMetadata::fromFile, ModLoaderType.FORGE),
pair(FabricModMetadata::fromFile, ModLoaderType.FABRIC),
pair(QuiltModMetadata::fromFile, ModLoaderType.QUILT),
@@ -365,14 +365,16 @@ public final class ModManager {
}
/**
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled or not
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled,upgraded or not
*
* @param fileName name of the file whose existence is being checked
* @return true if the file exists
*/
public boolean hasSimpleMod(String fileName) {
return Files.exists(getModsDirectory().resolve(StringUtils.removeSuffix(fileName, DISABLED_EXTENSION)))
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION)));
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION)))
|| Files.exists(getModsDirectory().resolve(StringUtils.removeSuffix(fileName, OLD_EXTENSION)))
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, OLD_EXTENSION)));
}
public Path getSimpleModPath(String fileName) {
@@ -24,6 +24,8 @@ 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.StringUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -211,7 +213,13 @@ public class CurseAddon implements RemoteMod.IMod {
}
public RemoteMod toMod() {
String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse("");
String iconUrl = "";
if (logo != null) {
if (StringUtils.isNotBlank(logo.getThumbnailUrl()))
iconUrl = logo.getThumbnailUrl();
else if (StringUtils.isNotBlank(logo.getUrl()))
iconUrl = logo.getUrl();
}
return new RemoteMod(
slug,
@@ -585,12 +593,12 @@ 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);
else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT);
else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGED);
else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGE);
else return Stream.empty();
}).collect(Collectors.toList())
);
@@ -188,9 +188,13 @@ public final class CurseCompletionTask extends Task<Void> {
String fileName = file.fileName();
return switch (classID) {
case 12, // Resource pack
6552 -> { // Shader pack
Path res = (classID == 12 ? resourcePacksRoot : shaderPacksRoot).resolve(fileName);
yield Files.exists(res) ? null : res;
6945 -> { // Data pack
Path path = resourcePacksRoot.resolve(fileName);
yield Files.exists(path) ? null : path;
}
case 6552 -> { // Shader pack
Path path = shaderPacksRoot.resolve(fileName);
yield Files.exists(path) ? null : path;
}
default -> {
if (modManager.hasSimpleMod(fileName)) {
@@ -24,7 +24,6 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.JsonAdapter;
import com.moandjiezana.toml.Toml;
import kala.compress.archivers.zip.ZipArchiveEntry;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
@@ -35,6 +34,10 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.tree.ZipFileTree;
import org.tomlj.Toml;
import org.tomlj.TomlArray;
import org.tomlj.TomlParseResult;
import org.tomlj.TomlTable;
import java.io.IOException;
import java.io.InputStream;
@@ -42,8 +45,8 @@ import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
@@ -171,15 +174,15 @@ public final class ForgeNewModMetadata {
}
public static LocalModFile fromNeoForgeFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException {
return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGED);
return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGE);
}
private static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException {
if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGED) {
if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGE) {
throw new IOException("Invalid mod loader: " + modLoaderType);
}
if (modLoaderType == ModLoaderType.NEO_FORGED) {
if (modLoaderType == ModLoaderType.NEO_FORGE) {
try {
return fromFile0("META-INF/neoforge.mods.toml", modLoaderType, modManager, modFile, tree);
} catch (Exception ignored) {
@@ -208,10 +211,15 @@ public final class ForgeNewModMetadata {
ZipArchiveEntry modToml = tree.getEntry(tomlPath);
if (modToml == null)
throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
Toml toml = new Toml().read(tree.readTextEntry(modToml));
ForgeNewModMetadata metadata = toml.to(ForgeNewModMetadata.class);
TomlParseResult tomlParseResult = Toml.parse(tree.readTextEntry(modToml));
if (tomlParseResult.hasErrors()) {
var ioException = new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
tomlParseResult.errors().forEach(ioException::addSuppressed);
throw ioException;
}
ForgeNewModMetadata metadata = JsonUtils.GSON.fromJson(tomlParseResult.toJson(), ForgeNewModMetadata.class);
if (metadata == null || metadata.getMods().isEmpty())
throw new IOException("Mod " + modFile + " `mods.toml` is malformed..");
throw new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
Mod mod = metadata.getMods().get(0);
ZipArchiveEntry manifestMF = tree.getEntry("META-INF/MANIFEST.MF");
String jarVersion = "";
@@ -224,7 +232,7 @@ public final class ForgeNewModMetadata {
}
}
ModLoaderType type = analyzeLoader(toml, mod.getModId(), modLoaderType);
ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
@@ -293,23 +301,32 @@ public final class ForgeNewModMetadata {
throw new IOException();
}
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) {
List<HashMap<String, Object>> dependencies = null;
private static ModLoaderType analyzeLoader(TomlParseResult toml, String modID, ModLoaderType loader) {
List<Map<String, Object>> dependencies = null;
try {
dependencies = toml.getList("dependencies." + modID);
TomlArray tomlArray = toml.getArray("dependencies." + modID);
if (tomlArray != null) {
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
}
} catch (ClassCastException ignored) { // https://github.com/HMCL-dev/HMCL/issues/5068
}
if (dependencies == null) {
try {
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
TomlArray tomlArray = toml.getArray("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
if (tomlArray != null) {
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
}
} catch (ClassCastException e) {
try {
Toml table = toml.getTable("dependencies");
TomlTable table = toml.getTable("dependencies");
if (table == null)
return loader;
dependencies = table.getList(modID);
TomlArray tomlArray = table.getArray(modID);
if (tomlArray != null) {
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
}
} catch (Throwable ignored) {
}
}
@@ -321,13 +338,13 @@ public final class ForgeNewModMetadata {
ModLoaderType result = null;
loop:
for (HashMap<String, Object> dependency : dependencies) {
for (Map<String, Object> dependency : dependencies) {
switch ((String) dependency.get("modId")) {
case "forge":
result = ModLoaderType.FORGE;
break loop;
case "neoforge":
result = ModLoaderType.NEO_FORGED;
result = ModLoaderType.NEO_FORGE;
break loop;
}
}
@@ -24,11 +24,7 @@ import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -42,6 +38,7 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -63,6 +60,41 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
public static final ModrinthRemoteModRepository RESOURCE_PACKS = new ModrinthRemoteModRepository("resourcepack");
public static final ModrinthRemoteModRepository SHADER_PACKS = new ModrinthRemoteModRepository("shader");
private static final Comparator<String> TAG_COMPARATOR = PriorityComparator.of(
List.of("babric",
"bta-babric",
"bukkit",
"bungeecord",
"canvas",
"datapack",
"fabric",
"folia",
"forge",
"geyser",
"iris",
"java-agent",
"legacy-fabric",
"liteloader",
"minecraft",
"modloader",
"mrpack",
"neoforge",
"nilloader",
"optifine",
"ornith",
"paper",
"purpur",
"quilt",
"rift",
"spigot",
"sponge",
"vanilla",
"velocity",
"waterfall"),
Comparator.naturalOrder(),
false
);
private static final Semaphore SEMAPHORE = new Semaphore(16);
private static final String PREFIX = "https://api.modrinth.com";
@@ -95,6 +127,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
}
}
static List<String> sortDisplayCategories(List<String> displayCategories) {
return displayCategories != null && !displayCategories.isEmpty()
? displayCategories.stream().sorted(TAG_COMPARATOR).toList()
: List.of();
}
@Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
SEMAPHORE.acquireUninterruptibly();
@@ -630,7 +668,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
loaders.stream().flatMap(loader -> {
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGED);
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE);
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER);
else return Stream.empty();
@@ -811,7 +849,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
author,
title,
description,
displayCategories,
sortDisplayCategories(displayCategories),
String.format("https://modrinth.com/%s/%s", projectType, projectId),
iconUrl,
this
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more