Compare commits

..
Author SHA1 Message Date
Glavo 5abc1f1f9d 修复“不自动添加 Java 虚拟机优化参数”选项未保存至配置文件的问题 (#4583) 2025-10-02 20:47:34 +08:00
Glavo 40b4ecd42a 从 synchronized 切换至 ReentrantLock (#4564) 2025-10-02 15:50:56 +08:00
Glavo e0f5c4d91e 优化 EventBus (#4565) 2025-10-02 15:38:03 +08:00
辞庐 35ecd58460 Fix #4556 : 修复在短时间内连续点击 OptionToggleButton 只会更改一次状态的问题 (#4568) 2025-10-02 15:31:20 +08:00
辞庐 f29c2bc202 Fix 4555: 修复外观 - 不透明度滑块 刻度线在不同滑块位置时排布不统一的问题 (#4571) 2025-10-02 15:25:52 +08:00
Glavo 361e928e73 支持从 WorkflowMultiBranchProject 中检查最新版本 (#4576) 2025-10-02 15:22:15 +08:00
Glavo 484b65b513 优化对颠倒的英语的支持 (#4577) 2025-10-02 15:21:03 +08:00
3gf8jv4dv 19f02e3967 chore: update versions.txt to 1.21.9 (#4566) 2025-10-01 13:08:43 +08:00
Glavo 495da6b6b0 更新 Jenkins CI 服务器的发布模型 (#4559) 2025-10-01 13:04:19 +08:00
Glavo 168c4ac708 支持通过环境变量控制 UI 缩放比例 (#4521) 2025-09-29 16:51:57 +08:00
Glavo 7e8a6679cc 将 EventManager 中的 synchronized 替换为 ReentrantLock (#4562) 2025-09-29 15:49:18 +08:00
Glavo e90d5185f6 增强 ArchiveFileTree (#4547) 2025-09-29 15:19:37 +08:00
Glavo 8a89baee09 使用 Gradle 获取更新信息 (#4560) 2025-09-28 20:29:07 +08:00
辞庐 7f4c5fc4a0 清理代码 (#4519) 2025-09-28 00:01:00 +08:00
Glavo 75789c774d 修复切换下载源后需要重启启动器才会生效的问题 (#4296) 2025-09-27 16:26:38 +08:00
Glavo f0a16b3d14 GitHub Action 按文件类型分别上传构建产物 (#4537) 2025-09-27 15:56:28 +08:00
Glavo 710e69751f 重构 FetchTask 以使用 HttpClient 和虚拟线程 (#4546) 2025-09-26 20:54:22 +08:00
Glavo 938d8647ce 删除 InvocationDispatcher 中的 synchronized 块 (#4550) 2025-09-26 20:44:34 +08:00
Glavo 4b4dd592c3 将 Schedulers.io() 切换至虚拟线程 (#4548) 2025-09-26 20:12:01 +08:00
Glavo 225dba7448 Bump JNA to 5.18.0 (#4549) 2025-09-26 17:06:01 +08:00
Glavo 5b1414428e 添加 Debug_zh.md (#4542) 2025-09-24 15:22:50 +08:00
Glavo cb467ae240 优化本地化支持 (#4539)
- 简化代码
- 修复错误用词
- 优化语言名称显示
2025-09-23 15:47:46 +08:00
rootwhois 9416c0cfb3 修复日志页面选中条目时 hover 背景闪烁两次的问题 (#4517)
fix #4245
2025-09-23 11:46:18 +08:00
3gf8jv4dv 16ea98c95a chore(l10n): revise chinese (traditional) and english (#4530) 2025-09-23 11:30:54 +08:00
3gf8jv4dv a8552a92d1 chore(l10n): add new translations to spanish (#4531) 2025-09-23 11:30:03 +08:00
Glavo 36d71bd14e 支持颠倒的英语 (#4527) 2025-09-22 22:07:06 +08:00
3gf8jv4dv cbe6554390 feat(l10n): add boot_es.properties (#4532) 2025-09-22 21:48:09 +08:00
Glavo 1b65e0b6d7 Fix #4526: 修复将空字符串注册为 Access Token 后日志输出异常的问题 (#4528) 2025-09-22 16:07:16 +08:00
Glavo bd9ae189f8 优化本地化支持 (#4525) 2025-09-21 15:15:22 +08:00
Glavo 867a04dfba 支持在“实例管理”按钮上通过滚动滚轮切换游戏实例 (#4509) 2025-09-20 20:15:11 +08:00
Wulian233 dd7703c7a9 修复删除世界时无法删除 zip 格式世界的问题 (#4500) 2025-09-20 17:14:19 +08:00
Glavo 756b95ca8c 添加本地化说明文档 (#4516) 2025-09-20 16:41:58 +08:00
rootwhoisandGlavo ac0a0c7604 修复 AdvancedListBox 在非必要的情况下显示滚动条的问题 (#4514)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-20 16:28:59 +08:00
Glavo 9ee611c0f6 Bump Gradle to 9.1.0 (#4520) 2025-09-20 15:55:20 +08:00
Glavo c38371f454 优化 Java 自动选择规则 (#4515) 2025-09-19 21:35:44 +08:00
Glavo eb3be8c465 在未启用 JIT 或硬件渲染加速的环境中默认禁用动画 (#4510) 2025-09-19 16:01:39 +08:00
Glavo e6ac07f62b 修复使用 JavaFX 25 时游戏下载界面布局错误的问题 (#4508) 2025-09-18 21:11:20 +08:00
Glavo a24fea4a95 支持禁止生成 JVM 优化参数 (#4507) 2025-09-18 20:04:25 +08:00
Glavo 8bd104c207 使用 Java 25 启动游戏时默认开启 Compact Object Headers (#4506) 2025-09-18 17:24:59 +08:00
Glavo a14a265986 Bump JavaFX to 21.0.8/25 (#4454) 2025-09-18 17:09:48 +08:00
Glavo 66524613b4 支持以类型安全的方式读写 Navigation#getSettings() (#4504) 2025-09-18 16:58:07 +08:00
Glavo a0568e34a8 清理残留的 java.io.File 用例 (#4503) 2025-09-18 15:26:03 +08:00
Glavo 45f7719096 Close #2987: 从 java.io.File 迁移至 NIO (#4502) 2025-09-17 21:41:17 +08:00
Glavo 77fadf5d28 将 org.jackhuang.hmcl.ui 从 java.io.File 迁移至 NIO (#4501) 2025-09-17 21:14:16 +08:00
Glavo e9d7d0a33c 将 org.jackhuang.hmcl.mod 从 java.io.File 迁移至 NIO (#4499) 2025-09-17 19:29:59 +08:00
Glavo 94994ffdb6 将 org.jackhuang.hmcl.setting 从 java.io.File 迁移至 NIO (#4498) 2025-09-17 18:39:34 +08:00
Glavo 26e274a89a 将 GameRepository 从 java.io.File 迁移至 NIO (#4496)
https://github.com/HMCL-dev/HMCL/issues/2987
2025-09-17 16:07:59 +08:00
Glavo 6a497df0d1 清理 org.jackhuang.hmcl.download.game (#4495)
1. 将 `java.io.File` 迁移至 NIO;
2. 将部分类迁移至 record。
2025-09-16 21:22:02 +08:00
Glavo fc1cce5e5a 删除 FileUtils 中基于 java.io.File 的工具方法 (#4493) 2025-09-16 20:52:44 +08:00
Glavo b328ed2cc9 Fix #4481: 修复 .hmcl 目录为跨驱动器符号链接时无法卸载 Java 的问题 (#4492) 2025-09-16 19:46:59 +08:00
Glavo 5a8d567bd7 将 isNameValid 方法移动至 FileUtils (#4491) 2025-09-16 16:11:38 +08:00
Glavo 5005343d00 缓存文件时遵循 Cache-Control 设置 (#4462) 2025-09-16 15:15:30 +08:00
Glavo e4bc8f48c5 Bump Gson to 2.13.2 (#4490) 2025-09-16 14:55:59 +08:00
ZkiteflyandGlavo 831a8a9bb8 fix(fmllib): Forge 1.5.2 安装时下载 fmllib,修复无法启动的问题 (#3976)
Fix #3975

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-16 14:38:44 +08:00
7c024473d6 世界管理新增删除世界功能 (#4263)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-15 23:06:30 +08:00
Glavo f594abc37d 更新 HMCLBoot 以使用地区中立的文件命名 (#4488) 2025-09-15 21:28:35 +08:00
Glavo b66e249591 在未启用 JIT 或硬件渲染加速的环境中启动时弹出提示 (#4487) 2025-09-15 21:26:47 +08:00
Glavo 15e490f313 Convert Pair to record (#4485) 2025-09-15 15:31:17 +08:00
Glavo 7ce59cac2c 清理 org.jackhuang.hmcl.util.logging (#4484) 2025-09-15 15:21:14 +08:00
Glavo a7178802f8 Create FileUtils#deleteDirectory(Path) (#4482) 2025-09-14 21:08:05 +08:00
Glavo 51954163d4 创建 RawPreservingProperty 接口 (#4464) 2025-09-14 21:06:32 +08:00
Wulian233 19072bd617 修复download.hint翻译条目未添加光影的说明的问题 (#4474) 2025-09-14 20:54:28 +08:00
Burning_TNT a39a23f938 Fix: 无法安装带有 Fabric 的非标准 MMC 整合包 (#4034)
- 修复无法安装带有 Fabric 的非标准 MMC 整合包的漏洞
- 在安装时写入当前安装器实现信息和启动器信息,以在后续调试时获得更多信息

Fix #4049
2025-09-14 20:50:53 +08:00
Glavo a10e9a04b1 创建 OSVersion 工具类 (#4480) 2025-09-14 20:13:43 +08:00
272 changed files with 6389 additions and 3158 deletions

No files matched your search

+1
View File
@@ -1071,6 +1071,7 @@ ij_jsp_keep_indents_on_empty_lines = false
ij_jspx_keep_indents_on_empty_lines = false
[{*.markdown,*.md}]
max_line_length = 200
ij_markdown_force_one_space_after_blockquote_symbol = true
ij_markdown_force_one_space_after_header_symbol = true
ij_markdown_force_one_space_after_list_bullet = true
+15 -34
View File
@@ -13,26 +13,16 @@ jobs:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
run: ./gradlew checkUpdateDev --no-daemon --info --stacktrace
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
@@ -78,26 +68,17 @@ jobs:
needs: dev-check-update
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
run: ./gradlew checkUpdateStable --no-daemon --info --stacktrace
- name: Check for existing tags
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
+19 -3
View File
@@ -27,8 +27,24 @@ jobs:
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload Artifacts
- name: Upload JAR
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
name: HMCL-${{ env.SHORT_SHA }}-jar
path: |
HMCL/build/libs/HMCL-*.jar
HMCL/build/libs/HMCL-*.jar.sha256
- name: Upload EXE
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-exe
path: |
HMCL/build/libs/HMCL-*.exe
HMCL/build/libs/HMCL-*.exe.sha256
- name: Upload SH
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-sh
path: |
HMCL/build/libs/HMCL-*.sh
HMCL/build/libs/HMCL-*.sh.sha256
+5 -1
View File
@@ -2,6 +2,7 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*.hprof
.gradle
@@ -14,14 +15,15 @@ hs_err_pid*
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
/.java/
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLMultiMCBootstrap/build/
/buildSrc/build/
# idea
@@ -30,12 +32,14 @@ hmcl-exported-logs-*
/HMCL/out/
/HMCLCore/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/HMCLMultiMCBootstrap/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
.classpath
.project
.settings
+63 -19
View File
@@ -1,5 +1,11 @@
import org.jackhuang.hmcl.gradle.CheckTranslations
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
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.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -13,30 +19,36 @@ plugins {
alias(libs.plugins.shadow)
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
version = "$versionRoot.$buildNumber"
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
if (buildNumber != null) {
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
"$versionRoot.0.$buildNumber"
} else {
"$versionRoot.$buildNumber"
}
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
version = if (shortCommit.isNullOrBlank()) {
"$versionRoot.SNAPSHOT"
} else if (isOfficial) {
"$versionRoot.dev-$shortCommit"
} else {
"$versionRoot.unofficial-$shortCommit"
}
}
val embedResources by configurations.registering
@@ -47,7 +59,7 @@ dependencies {
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
if (launcherExe == null) {
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
@@ -192,7 +204,7 @@ tasks.shadowJar {
"Enable-Native-Access" to "ALL-UNNAMED"
)
if (launcherExe != null) {
if (launcherExe.isNotBlank()) {
into("assets") {
from(file(launcherExe))
}
@@ -206,11 +218,20 @@ tasks.shadowJar {
tasks.processResources {
dependsOn(createPropertiesFile)
dependsOn(upsideDownTranslate)
dependsOn(createLocaleNamesResourceBundle)
dependsOn(createLanguageList)
into("assets/") {
from(hmclPropertiesFile)
from(embedResources)
}
into("assets/lang") {
from(createLanguageList.map { it.outputFile })
from(upsideDownTranslate.map { it.outputFile })
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
}
}
val makeExecutables by tasks.registering {
@@ -344,6 +365,29 @@ tasks.register<CheckTranslations>("checkTranslations") {
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
}
// l10n
val generatedDir = layout.buildDirectory.dir("generated")
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
}
val createLanguageList by tasks.registering(CreateLanguageList::class) {
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
resourceBundleBaseName.set("I18N")
additionalLanguages.set(listOf("en-Qabs"))
outputFile.set(generatedDir.map { it.file("languages.json") })
}
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
dependsOn(createLanguageList)
languagesFile.set(createLanguageList.flatMap { it.outputFile })
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
}
// mcmod data
tasks.register<ParseModDataTask>("parseModData") {
@@ -46,21 +46,7 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
System.getProperties().putIfAbsent("prism.forceGPU", "true");
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
setupJavaFXVMOptions();
checkDirectoryPath();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
@@ -80,6 +66,71 @@ public final class EntryPoint {
System.exit(exitCode);
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
try {
@@ -75,13 +75,7 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
try {
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
} catch (Throwable e) {
LOG.warning("Failed to get prism pipeline", e);
}
LOG.info("Prism Pipeline: " + FXUtils.GRAPHICS_PIPELINE);
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
@@ -243,8 +237,8 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion()
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + ')'));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
}
@@ -42,7 +42,7 @@ public class CrashReport {
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
@@ -64,7 +63,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
if (config().isDisableAutoGameOptions())
return;
Path runDir = repository.getRunDirectory(version.getId()).toPath();
Path runDir = repository.getRunDirectory(version.getId());
Path optionsFile = runDir.resolve("options.txt");
Path configFolder = runDir.resolve("config");
@@ -81,8 +80,6 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
Locale locale = Locale.getDefault();
if (LocaleUtils.isEnglish(locale))
return;
/*
* 1.0 : No language option, do not set for these versions
@@ -113,7 +110,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
String region = locale.getCountry();
return switch (LocaleUtils.getISO1Language(locale)) {
return switch (LocaleUtils.getRootLanguage(locale)) {
case "es" -> "es_ES";
case "ja" -> "ja_JP";
case "ru" -> "ru_RU";
@@ -130,6 +127,13 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
yield "zh_CN";
}
case "en" -> {
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
yield "en_UD";
}
yield "";
}
default -> "";
};
}
@@ -141,7 +145,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
@Override
public void makeLaunchScript(File scriptFile) throws IOException {
public void makeLaunchScript(Path scriptFile) throws IOException {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
@@ -44,9 +44,9 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
@@ -57,7 +57,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class HMCLGameRepository extends DefaultGameRepository {
public final class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
// local version settings
@@ -66,7 +66,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
public HMCLGameRepository(Profile profile, Path baseDirectory) {
super(baseDirectory);
this.profile = profile;
}
@@ -85,18 +85,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
@Override
public File getRunDirectory(String id) {
public Path getRunDirectory(String id) {
switch (getGameDirectoryType(id)) {
case VERSION_FOLDER:
return getVersionRoot(id);
case ROOT_FOLDER:
return super.getRunDirectory(id);
case CUSTOM:
File dir = new File(getVersionSetting(id).getGameDir());
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
return dir;
try {
return Path.of(getVersionSetting(id).getGameDir());
} catch (InvalidPathException ignored) {
return getVersionRoot(id);
}
default:
throw new Error();
throw new AssertionError("Unreachable");
}
}
@@ -119,22 +121,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
});
try {
File file = new File(getBaseDirectory(), "launcher_profiles.json");
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
Path file = getBaseDirectory().resolve("launcher_profiles.json");
if (!Files.exists(file) && !versions.isEmpty()) {
Files.createDirectories(file.getParent());
Files.writeString(file, PROFILE);
}
} catch (IOException ex) {
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
}
public void changeDirectory(File newDirectory) {
public void changeDirectory(Path newDirectory) {
setBaseDirectory(newDirectory);
refreshVersionsAsync().start();
}
private void clean(File directory) throws IOException {
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
FileUtils.deleteDirectory(new File(directory, "logs"));
private void clean(Path directory) throws IOException {
FileUtils.deleteDirectory(directory.resolve("crash-reports"));
FileUtils.deleteDirectory(directory.resolve("logs"));
}
public void clean(String id) throws IOException {
@@ -143,8 +147,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId).toPath();
Path dstDir = getVersionRoot(dstId).toPath();
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
Version fromVersion = getVersion(srcId);
@@ -178,22 +182,22 @@ public class HMCLGameRepository extends DefaultGameRepository {
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
saveVersionSetting(dstId);
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
Path srcGameDir = getRunDirectory(srcId);
Path dstGameDir = getRunDirectory(dstId);
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null));
}
private File getLocalVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg");
private Path getLocalVersionSettingFile(String id) {
return getVersionRoot(id).resolve("hmclversion.cfg");
}
private void loadLocalVersionSetting(String id) {
File file = getLocalVersionSettingFile(id);
if (file.exists())
Path file = getLocalVersionSettingFile(id);
if (Files.exists(file))
try {
VersionSetting versionSetting = GSON.fromJson(Files.readString(file.toPath()), VersionSetting.class);
VersionSetting versionSetting = JsonUtils.fromJsonFile(file, VersionSetting.class);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
@@ -256,12 +260,12 @@ public class HMCLGameRepository extends DefaultGameRepository {
return vs;
}
public Optional<File> getVersionIconFile(String id) {
File root = getVersionRoot(id);
public Optional<Path> getVersionIconFile(String id) {
Path root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
File file = new File(root, "icon." + extension);
if (file.exists()) {
Path file = root.resolve("icon." + extension);
if (Files.exists(file)) {
return Optional.of(file);
}
}
@@ -269,7 +273,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
return Optional.empty();
}
public void setVersionIconFile(String id, File iconFile) throws IOException {
public void setVersionIconFile(String id, Path iconFile) throws IOException {
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
throw new IllegalArgumentException("Unsupported icon file: " + ext);
@@ -277,13 +281,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
deleteIconFile(id);
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext));
}
public void deleteIconFile(String id) {
File root = getVersionRoot(id);
Path root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
new File(root, "icon." + extension).delete();
Path file = root.resolve("icon." + extension);
try {
Files.deleteIfExists(file);
} catch (IOException e) {
LOG.warning("Failed to delete icon file: " + file, e);
}
}
}
@@ -296,10 +305,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
Optional<File> iconFile = getVersionIconFile(id);
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get().toPath());
return FXUtils.loadImage(iconFile.get());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -334,7 +343,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
public void saveVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
return;
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
Path file = getLocalVersionSettingFile(id).toAbsolutePath().normalize();
try {
Files.createDirectories(file.getParent());
} catch (IOException e) {
@@ -368,7 +377,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -379,7 +388,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int) (getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
@@ -404,6 +413,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNoGeneratedOptimizingJVMArgs(vs.isNoOptimizingJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
@@ -425,15 +435,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
File json = getModpackConfiguration(version);
if (json.exists()) {
Path json = getModpackConfiguration(version);
if (Files.exists(json)) {
try {
String jsonText = Files.readString(json.toPath());
String jsonText = Files.readString(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
} catch (IOException | JsonParseException e) {
e.printStackTrace();
LOG.warning("Failed to parse modpack configuration file " + json, e);
}
}
@@ -444,8 +454,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
@Override
public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg");
public Path getModpackConfiguration(String version) {
return getVersionRoot(version).resolve("modpack.cfg");
}
public void markVersionAsModpack(String id) {
@@ -458,16 +468,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
public void markVersionLaunchedAbnormally(String id) {
try {
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
Files.createFile(getVersionRoot(id).resolve(".abnormal"));
} catch (IOException ignored) {
}
}
public boolean unmarkVersionLaunchedAbnormally(String id) {
File file = new File(getVersionRoot(id), ".abnormal");
boolean result = file.isFile();
file.delete();
return result;
Path file = getVersionRoot(id).resolve(".abnormal");
if (Files.isRegularFile(file)) {
try {
Files.delete(file);
} catch (IOException e) {
LOG.warning("Failed to delete abnormal mark file: " + file, e);
}
return true;
} else {
return false;
}
}
private static final Gson GSON = new GsonBuilder()
@@ -489,7 +507,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return OperatingSystem.isNameValid(id);
return FileUtils.isNameValid(id);
}
/**
@@ -29,14 +29,15 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final Path zipFile;
private final String name;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
@@ -44,16 +45,16 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
public HMCLModpackInstallTask(Profile profile, Path zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
Path run = repository.getRunDirectory(name);
Path json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && Files.notExists(json))
throw new IllegalArgumentException("Version " + name + " already exists");
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
@@ -64,8 +65,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(Modpack.class));
if (Files.exists(json)) {
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@@ -49,15 +48,14 @@ public final class HMCLModpackProvider implements ModpackProvider {
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository repository)) {
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
}
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
Profile profile = repository.getProfile();
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
@@ -79,9 +77,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
return manifest;
}
private static class HMCLModpack extends Modpack {
private final static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
@@ -49,15 +49,16 @@ import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.lang.ref.WeakReference;
@@ -76,7 +77,7 @@ public final class LauncherHelper {
private final Profile profile;
private Account account;
private final String selectedVersion;
private File scriptFile;
private Path scriptFile;
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
@@ -119,9 +120,8 @@ public final class LauncherHelper {
launch0();
}
public void makeLaunchScript(File scriptFile) {
public void makeLaunchScript(Path scriptFile) {
this.scriptFile = Objects.requireNonNull(scriptFile);
launch();
}
@@ -160,19 +160,17 @@ public final class LauncherHelper {
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& OperatingSystem.WINDOWS_VERSION != null) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.getAbsolutePath().indexOf('=') >= 0) {
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
String agent = file.getAbsolutePath() + "=" + renderer.name().toLowerCase(Locale.ROOT);
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
@@ -226,7 +224,7 @@ public final class LauncherHelper {
} else {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
});
}
}).withFakeProgress(
@@ -709,6 +707,7 @@ public final class LauncherHelper {
*/
private final class HMCLProcessListener implements ProcessListener {
private final ReentrantLock lock = new ReentrantLock();
private final HMCLGameRepository repository;
private final Version version;
private final LaunchOptions launchOptions;
@@ -852,21 +851,27 @@ public final class LauncherHelper {
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
lock.lock();
try {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
} finally {
lock.unlock();
}
}
if (!lwjgl) {
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
synchronized (this) {
lock.lock();
try {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
} finally {
lock.unlock();
}
}
}
@@ -891,9 +896,12 @@ public final class LauncherHelper {
// Game crashed before opening the game window.
if (!lwjgl) {
synchronized (this) {
lock.lock();
try {
if (!lwjgl)
finishLaunch();
} finally {
lock.unlock();
}
}
@@ -44,8 +44,8 @@ public final class LogExporter {
}
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
Path runDirectory = gameRepository.getRunDirectory(versionId);
Path baseDirectory = gameRepository.getBaseDirectory();
List<String> versions = new ArrayList<>();
String currentVersionId = versionId;
@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
import org.jackhuang.hmcl.mod.multimc.MultiMCComponents;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
@@ -42,7 +44,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -58,7 +59,8 @@ import static org.jackhuang.hmcl.util.Lang.toIterable;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModpackHelper {
private ModpackHelper() {}
private ModpackHelper() {
}
private static final Map<String, ModpackProvider> providers = mapOf(
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
@@ -69,12 +71,16 @@ public final class ModpackHelper {
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
);
static {
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
}
@Nullable
public static ModpackProvider getProviderByType(String type) {
return providers.get(type);
}
public static boolean isFileModpackByExtension(File file) {
public static boolean isFileModpackByExtension(Path file) {
String ext = FileUtils.getExtension(file);
return "zip".equals(ext) || "mrpack".equals(ext);
}
@@ -132,15 +138,12 @@ public final class ModpackHelper {
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.fromJsonFile(file.toPath(), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
try {
return JsonUtils.fromJsonFile(file, ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
}
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
@@ -171,21 +174,21 @@ public final class ModpackHelper {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(name, location.toFile());
Profile newProfile = new Profile(name, location);
newProfile.setUseRelativePath(true);
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -230,8 +233,8 @@ public final class ModpackHelper {
}
}
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
@@ -207,7 +207,7 @@ public final class HMCLJavaRepository implements JavaRepository {
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
return Task.runAsync(() -> {
Files.deleteIfExists(getManifestFile(platform, name));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
FileUtils.deleteDirectory(getJavaDir(platform, name));
});
}
@@ -220,7 +220,7 @@ public final class HMCLJavaRepository implements JavaRepository {
if (relativized.getNameCount() > 1) {
String name = relativized.getName(0).toString();
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name));
}
});
}
@@ -20,10 +20,13 @@ package org.jackhuang.hmcl.java;
import kala.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -98,8 +101,7 @@ public final class JavaInstallTask extends Task<JavaManifest> {
}
if (tree.isExecutable(entry))
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
FileUtils.setExecutable(path);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(HexFormat.of().formatHex(messageDigest.digest()), size));
}
@@ -209,9 +209,18 @@ public final class JavaManager {
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
assert java.isManaged();
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
Path platformRoot;
try {
platformRoot = REPOSITORY.getPlatformRoot(java.getPlatform()).toRealPath();
} catch (Throwable ignored) {
return Task.completed(null);
}
if (!java.getBinary().startsWith(platformRoot))
return Task.completed(null);
Path relativized = platformRoot.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
FXUtils.runInFX(() -> {
try {
@@ -255,16 +264,15 @@ public final class JavaManager {
}
}
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
if (java1 == null)
return java2;
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
if (java1.getParsedVersion() != java2.getParsedVersion())
// Prefer the Java version that is closer to the game's recommended Java version
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
else
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
}
@Nullable
@@ -280,9 +288,6 @@ public final class JavaManager {
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
@@ -310,15 +315,10 @@ public final class JavaManager {
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
mandatory = chooseJava(mandatory, java);
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
if (!violationSuggested)
suggested = chooseJava(suggested, java);
}
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
@@ -37,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonSerializable
public final class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
@@ -34,20 +34,18 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.gson.ObservableField;
import org.jackhuang.hmcl.util.gson.PaintAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.*;
import java.net.Proxy;
import java.nio.file.Path;
import java.util.*;
@JsonAdapter(value = Config.Adapter.class)
@@ -57,7 +55,7 @@ public final class Config implements Observable {
public static final int CURRENT_UI_VERSION = 0;
public static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(Path.class, PathTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
@@ -223,7 +221,7 @@ public final class Config implements Observable {
}
@SerializedName("localization")
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(SupportedLocale.DEFAULT);
public ObjectProperty<SupportedLocale> localizationProperty() {
return localization;
@@ -260,7 +258,7 @@ public final class Config implements Observable {
}
@SerializedName("commonDirType")
private final ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
private final ObjectProperty<EnumCommonDirectory> commonDirType = new RawPreservingObjectProperty<>(EnumCommonDirectory.DEFAULT);
public ObjectProperty<EnumCommonDirectory> commonDirTypeProperty() {
return commonDirType;
@@ -367,7 +365,11 @@ public final class Config implements Observable {
}
@SerializedName("animationDisabled")
private final BooleanProperty animationDisabled = new SimpleBooleanProperty();
private final BooleanProperty animationDisabled = new SimpleBooleanProperty(
FXUtils.REDUCED_MOTION == Boolean.TRUE
|| !JavaRuntime.CURRENT_JIT_ENABLED
|| !FXUtils.GPU_ACCELERATION_ENABLED
);
public BooleanProperty animationDisabledProperty() {
return animationDisabled;
@@ -397,7 +399,7 @@ public final class Config implements Observable {
}
@SerializedName("backgroundType")
private final ObjectProperty<EnumBackgroundImage> backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT);
private final ObjectProperty<EnumBackgroundImage> backgroundImageType = new RawPreservingObjectProperty<>(EnumBackgroundImage.DEFAULT);
public ObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
return backgroundImageType;
@@ -769,9 +771,7 @@ public final class Config implements Observable {
for (var field : FIELDS) {
Observable observable = field.get(config);
if (config.tracker.isDirty(observable)) {
JsonElement serialized = field.serialize(config, context);
if (serialized != null && !serialized.isJsonNull())
result.add(field.getSerializedName(), serialized);
field.serialize(result, config, context);
}
}
config.unknownFields.forEach(result::add);
@@ -791,6 +791,14 @@ public final class Config implements Observable {
var values = new LinkedHashMap<>(json.getAsJsonObject().asMap());
for (ObservableField<Config> field : FIELDS) {
JsonElement value = values.remove(field.getSerializedName());
if (value == null) {
for (String alternateName : field.getAlternateNames()) {
value = values.remove(alternateName);
if (value != null)
break;
}
}
if (value != null) {
config.tracker.markDirty(field.get(config));
field.deserialize(config, value, context);
@@ -31,23 +31,21 @@ import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class DownloadProviders {
private DownloadProviders() {}
private DownloadProviders() {
}
private static DownloadProvider currentDownloadProvider;
private static final DownloadProviderWrapper provider;
public static final Map<String, DownloadProvider> providersById;
public static final Map<String, DownloadProvider> rawProviders;
@@ -59,6 +57,7 @@ public final class DownloadProviders {
public static final String DEFAULT_PROVIDER_ID = "balanced";
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
@SuppressWarnings("unused")
private static final InvalidationListener observer;
static {
@@ -68,71 +67,68 @@ public final class DownloadProviders {
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", BMCLAPI)
rawProviders = Map.of(
"mojang", MOJANG,
"bmclapi", BMCLAPI
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
fileProvider.setDownloadProviderCandidates(List.of(BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
providersById = mapOf(
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
providersById = Map.of(
"official", new AutoDownloadProvider(MOJANG, fileProvider),
"balanced", new AutoDownloadProvider(balanced, fileProvider),
"mirror", new AutoDownloadProvider(BMCLAPI, fileProvider));
observer = FXUtils.observeWeak(() -> {
FetchTask.setDownloadExecutorConcurrency(
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
provider = new DownloadProviderWrapper(MOJANG);
}
static void init() {
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
if (!providersById.containsKey(versionListSource)) {
config().setVersionListSource(DEFAULT_PROVIDER_ID);
return;
InvalidationListener onChangeDownloadSource = observable -> {
String versionListSource = Objects.requireNonNullElse(config().getVersionListSource(), "");
if (config().isAutoChooseDownloadType()) {
DownloadProvider currentDownloadProvider = providersById.get(versionListSource);
if (currentDownloadProvider == null)
currentDownloadProvider = Objects.requireNonNull(providersById.get(DEFAULT_PROVIDER_ID),
"default provider is null");
provider.setProvider(currentDownloadProvider);
} else {
provider.setProvider(fileDownloadProvider);
}
};
config().versionListSourceProperty().addListener(onChangeDownloadSource);
config().autoChooseDownloadTypeProperty().addListener(onChangeDownloadSource);
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
});
if (!rawProviders.containsKey(config().getDownloadType())) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
}
onChangeDownloadSource.invalidated(null);
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
fileDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
rawProviders.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
DownloadProvider primary = Objects.requireNonNullElseGet(
rawProviders.get(Objects.requireNonNullElse(downloadType, "")),
() -> rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
List<DownloadProvider> providers = new ArrayList<>(rawProviders.size());
providers.add(primary);
for (DownloadProvider provider : rawProviders.values()) {
if (provider != primary)
providers.add(provider);
}
fileDownloadProvider.setDownloadProviderCandidates(providers);
});
}
public static String getPrimaryDownloadProviderId() {
String downloadType = config().getDownloadType();
if (providersById.containsKey(downloadType))
return downloadType;
else
return DEFAULT_PROVIDER_ID;
}
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
return Optional.ofNullable(providersById.get(primaryId))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
}
/**
* Get current primary preferred download provider
*/
public static DownloadProvider getDownloadProvider() {
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
return provider;
}
public static String localizeErrorMessage(Throwable exception) {
@@ -35,8 +35,8 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
import java.lang.reflect.Type;
import java.nio.file.Path;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
@@ -65,17 +65,17 @@ public final class Profile implements Observable {
this.selectedVersion.set(selectedVersion);
}
private final ObjectProperty<File> gameDir;
private final ObjectProperty<Path> gameDir;
public ObjectProperty<File> gameDirProperty() {
public ObjectProperty<Path> gameDirProperty() {
return gameDir;
}
public File getGameDir() {
public Path getGameDir() {
return gameDir.get();
}
public void setGameDir(File gameDir) {
public void setGameDir(Path gameDir) {
this.gameDir.set(gameDir);
}
@@ -103,7 +103,7 @@ public final class Profile implements Observable {
this.name.set(name);
}
private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
private final BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
public BooleanProperty useRelativePathProperty() {
return useRelativePath;
@@ -118,18 +118,18 @@ public final class Profile implements Observable {
}
public Profile(String name) {
this(name, new File(".minecraft"));
this(name, Path.of(".minecraft"));
}
public Profile(String name, File initialGameDir) {
public Profile(String name, Path initialGameDir) {
this(name, initialGameDir, new VersionSetting());
}
public Profile(String name, File initialGameDir, VersionSetting global) {
public Profile(String name, Path initialGameDir, VersionSetting global) {
this(name, initialGameDir, global, null, false);
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
public Profile(String name, Path initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
@@ -234,7 +234,7 @@ public final class Profile implements Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("global", context.serialize(src.getGlobal()));
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
jsonObject.addProperty("gameDir", src.getGameDir().toString());
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
@@ -243,12 +243,11 @@ public final class Profile implements Observable {
@Override
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
if (!(json instanceof JsonObject obj)) return null;
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
return new Profile("Default",
new File(gameDir),
Path.of(gameDir),
context.deserialize(obj.get("global"), VersionSetting.class),
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -48,20 +48,17 @@ public final class Profiles {
}
public static String getProfileDisplayName(Profile profile) {
switch (profile.getName()) {
case Profiles.DEFAULT_PROFILE:
return i18n("profile.default");
case Profiles.HOME_PROFILE:
return i18n("profile.home");
default:
return profile.getName();
}
return switch (profile.getName()) {
case Profiles.DEFAULT_PROFILE -> i18n("profile.default");
case Profiles.HOME_PROFILE -> i18n("profile.home");
default -> profile.getName();
};
}
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
private static final ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
profiles.addListener(onInvalidating(this::invalidated));
}
@@ -104,8 +101,8 @@ public final class Profiles {
private static void checkProfiles() {
if (profiles.isEmpty()) {
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
Profile current = new Profile(Profiles.DEFAULT_PROFILE, Path.of(".minecraft"), new VersionSetting(), null, true);
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY);
Platform.runLater(() -> profiles.addAll(current, home));
}
}
@@ -380,6 +380,20 @@ public final class VersionSetting implements Cloneable, Observable {
noJVMArgsProperty.set(noJVMArgs);
}
private final BooleanProperty noOptimizingJVMArgsProperty = new SimpleBooleanProperty(this, "noOptimizingJVMArgs", false);
public BooleanProperty noOptimizingJVMArgsProperty() {
return noOptimizingJVMArgsProperty;
}
public boolean isNoOptimizingJVMArgs() {
return noOptimizingJVMArgsProperty.get();
}
public void setNoOptimizingJVMArgs(boolean noOptimizingJVMArgs) {
noOptimizingJVMArgsProperty.set(noOptimizingJVMArgs);
}
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
public BooleanProperty notCheckJVMProperty() {
@@ -757,6 +771,7 @@ public final class VersionSetting implements Cloneable, Observable {
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("noOptimizingJVMArgs", src.isNoOptimizingJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
@@ -827,6 +842,7 @@ public final class VersionSetting implements Cloneable, Observable {
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoOptimizingJVMArgs(Optional.ofNullable(obj.get("noOptimizingJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
@@ -62,7 +62,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -72,6 +72,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
public static final String JAVA_VERSION_TIP = "javaVersion";
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
public static final String SOFTWARE_RENDERING = "softwareRendering";
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
@@ -90,7 +92,7 @@ public final class Controllers {
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Path modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
@@ -347,6 +349,24 @@ public final class Controllers {
}
}
// Check whether JIT is enabled in the current environment
if (!JavaRuntime.CURRENT_JIT_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(JAVA_INTERPRETED_MODE_TIP))) {
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.java_interpreted_mode"), i18n("message.warning"), MessageType.WARNING)
.ok(null)
.addCancel(i18n("button.do_not_show_again"), () ->
config().getShownTips().put(JAVA_INTERPRETED_MODE_TIP, true))
.build());
}
// Check whether hardware acceleration is enabled
if (!FXUtils.GPU_ACCELERATION_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(SOFTWARE_RENDERING))) {
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.software_rendering"), i18n("message.warning"), MessageType.WARNING)
.ok(null)
.addCancel(i18n("button.do_not_show_again"), () ->
config().getShownTips().put(SOFTWARE_RENDERING, true))
.build());
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
@@ -49,9 +49,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.FileChooser;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.*;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
@@ -79,24 +77,25 @@ import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.WeakReference;
import java.net.*;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -109,6 +108,25 @@ public final class FXUtils {
public static final int JAVAFX_MAJOR_VERSION;
public static final String GRAPHICS_PIPELINE;
public static final boolean GPU_ACCELERATION_ENABLED;
static {
String pipelineName = "";
try {
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
if (pipeline != null) {
pipelineName = pipeline.getClass().getName();
}
} catch (Throwable e) {
LOG.warning("Failed to get prism pipeline", e);
}
GRAPHICS_PIPELINE = pipelineName;
GPU_ACCELERATION_ENABLED = !pipelineName.endsWith(".SWPipeline");
}
/// @see Platform.Preferences
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
public static final @Nullable ObservableBooleanValue DARK_MODE;
@@ -440,20 +458,27 @@ public final class FXUtils {
}
}
public static void openFolder(File file) {
if (!FileUtils.makeDirectory(file)) {
LOG.error("Unable to make directory " + file);
public static void openFolder(Path file) {
if (file.getFileSystem() != FileSystems.getDefault()) {
LOG.warning("Cannot open folder as the file system is not supported: " + file);
return;
}
String path = file.getAbsolutePath();
try {
Files.createDirectories(file);
} catch (IOException e) {
LOG.warning("Failed to create directory " + file);
return;
}
String path = FileUtils.getAbsolutePath(file);
String openCommand;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommand = "explorer.exe";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
openCommand = "/usr/bin/open";
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && Files.exists(Path.of("/usr/bin/xdg-open")))
openCommand = "/usr/bin/xdg-open";
else
openCommand = null;
@@ -475,7 +500,7 @@ public final class FXUtils {
// Fallback to java.awt.Desktop::open
try {
java.awt.Desktop.getDesktop().open(file);
java.awt.Desktop.getDesktop().open(file.toFile());
} catch (Throwable e) {
LOG.error("Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
@@ -518,11 +543,11 @@ public final class FXUtils {
}
// Fallback to open folder
openFolder(file.getParent().toFile());
openFolder(file.getParent());
});
} else {
// We do not have a universal method to show file in file manager.
openFolder(file.getParent().toFile());
openFolder(file.getParent());
}
}
@@ -1190,7 +1215,6 @@ public final class FXUtils {
public static JFXButton newBorderButton(String text) {
JFXButton button = new JFXButton(text);
button.getStyleClass().add("jfx-button-border");
button.setButtonType(JFXButton.ButtonType.RAISED);
return button;
}
@@ -1234,14 +1258,14 @@ public final class FXUtils {
}
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
public static void applyDragListener(Node node, PathMatcher filter, Consumer<List<Path>> callback) {
applyDragListener(node, filter, callback, null);
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback, Runnable dragDropped) {
public static void applyDragListener(Node node, PathMatcher filter, Consumer<List<Path>> callback, Runnable dragDropped) {
node.setOnDragOver(event -> {
if (event.getGestureSource() != node && event.getDragboard().hasFiles()) {
if (event.getDragboard().getFiles().stream().anyMatch(filter::accept))
if (event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(filter::matches))
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
event.consume();
@@ -1250,7 +1274,7 @@ public final class FXUtils {
node.setOnDragDropped(event -> {
List<File> files = event.getDragboard().getFiles();
if (files != null) {
List<File> acceptFiles = files.stream().filter(filter::accept).collect(Collectors.toList());
List<Path> acceptFiles = files.stream().map(File::toPath).filter(filter::matches).toList();
if (!acceptFiles.isEmpty()) {
callback.accept(acceptFiles);
event.setDropCompleted(true);
@@ -1342,6 +1366,27 @@ public final class FXUtils {
});
}
public static <T> void onScroll(Node node, List<T> list,
ToIntFunction<List<T>> finder,
Consumer<T> updater
) {
node.addEventHandler(ScrollEvent.SCROLL, event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
int index = finder.applyAsInt(list);
if (index < 0) return;
if (deltaY > 0) // up
index--;
else // down
index++;
updater.accept(list.get((index + list.size()) % list.size()));
event.consume();
});
}
public static void copyOnDoubleClick(Labeled label) {
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
@@ -129,7 +129,7 @@ public class GameCrashWindow extends Stage {
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
}), Task.supplyAsync(() -> {
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
Path latestLog = repository.getRunDirectory(version.getId()).resolve("logs/latest.log");
if (!Files.isReadable(latestLog)) {
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
@@ -379,7 +379,7 @@ public class GameCrashWindow extends Stage {
TwoLineListItem gameDir = new TwoLineListItem();
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
gameDir.setSubtitle(launchOptions.getGameDir().toAbsolutePath().toString());
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
TwoLineListItem javaDir = new TwoLineListItem();
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -318,7 +319,9 @@ public class InstallerItem extends Control {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
private static final WeakListenerHolder holder = new WeakListenerHolder();
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Number> holder;
InstallerItemSkin(InstallerItem control) {
super(control);
@@ -326,9 +329,10 @@ public class InstallerItem extends Control {
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
} else {
pane = new HBox();
holder = null;
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
@@ -64,11 +64,9 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
JFXButton btnAdd = new JFXButton();
JFXButton btnAdd = FXUtils.newRaisedButton("");
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.ADD.createIcon(Theme.whiteFill(), -1));
btnAdd.setOnAction(e -> skinnable.add());
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
@@ -311,6 +312,7 @@ public final class LogWindow extends Stage {
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
x.setSelectionModel(new NoneMultipleSelectionModel<>());
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
@@ -21,7 +21,6 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import org.jackhuang.hmcl.auth.Account;
@@ -77,25 +76,9 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setActionButtonVisible(false);
setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
Account current = account.get();
if (current == null) return;
ObservableList<Account> accounts = Accounts.getAccounts();
int currentIndex = accounts.indexOf(current);
if (currentIndex < 0) return;
if (deltaY > 0) // up
currentIndex--;
else // down
currentIndex++;
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
});
FXUtils.onScroll(this, Accounts.getAccounts(),
accounts -> accounts.indexOf(account.get()),
Accounts::setSelectedAccount);
}
public ObjectProperty<Account> accountProperty() {
@@ -41,13 +41,14 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
@@ -116,8 +117,7 @@ public class AccountListItem extends RadioButton {
}
public ObservableBooleanValue canUploadSkin() {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
if (account instanceof AuthlibInjectorAccount aiAccount) {
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
return createBooleanBinding(() -> {
Set<TextureType> uploadableTextures = profile.get()
@@ -148,7 +148,7 @@ public class AccountListItem extends RadioButton {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("account.skin.upload"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (selectedFile == null) {
return null;
}
@@ -156,7 +156,7 @@ public class AccountListItem extends RadioButton {
return refreshAsync()
.thenRunAsync(() -> {
Image skinImg;
try (FileInputStream input = new FileInputStream(selectedFile)) {
try (var input = Files.newInputStream(selectedFile)) {
skinImg = new Image(input);
} catch (IOException e) {
throw new InvalidSkinException("Failed to read skin image", e);
@@ -167,7 +167,7 @@ public class AccountListItem extends RadioButton {
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
account.uploadSkin(skin.isSlim(), selectedFile);
})
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
@@ -39,8 +39,9 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.UUID;
@@ -81,16 +82,16 @@ public class OfflineAccountSkinPane extends StackPane {
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
if (e.getDragboard().hasFiles()) {
File file = e.getDragboard().getFiles().get(0);
if (file.getAbsolutePath().endsWith(".png"))
Path file = e.getDragboard().getFiles().get(0).toPath();
if (FileUtils.getName(file).endsWith(".png"))
e.acceptTransferModes(TransferMode.COPY);
}
});
canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> {
if (e.isAccepted()) {
File skin = e.getDragboard().getFiles().get(0);
Path skin = e.getDragboard().getFiles().get(0).toPath();
Platform.runLater(() -> {
skinSelector.setValue(skin.getAbsolutePath());
skinSelector.setValue(FileUtils.getAbsolutePath(skin));
skinItem.setSelectedData(Skin.Type.LOCAL_FILE);
});
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.animation;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/**
@@ -37,7 +36,7 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled() && FXUtils.REDUCED_MOTION != Boolean.TRUE;
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -40,8 +41,16 @@ public class AdvancedListBox extends ScrollPane {
setFitToHeight(true);
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
setVbarPolicy(ScrollBarPolicy.NEVER);
container.getStyleClass().add("advanced-list-box-content");
this.addEventFilter(MouseEvent.MOUSE_ENTERED, event -> {
if (container.getHeight() > getHeight())
setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
});
this.addEventFilter(MouseEvent.MOUSE_EXITED,
event -> setVbarPolicy(ScrollBarPolicy.NEVER));
}
public AdvancedListBox add(Node child) {
@@ -27,17 +27,19 @@ import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class FileItem extends BorderPane {
private final Label lblPath = new Label();
@@ -77,14 +79,14 @@ public class FileItem extends BorderPane {
private String processPath(String path) {
Path given;
try {
given = Paths.get(path).toAbsolutePath();
given = Path.of(path).toAbsolutePath().normalize();
} catch (IllegalArgumentException e) {
return path;
}
if (isConvertToRelativePath()) {
try {
return Paths.get(".").normalize().toAbsolutePath().relativize(given).normalize().toString();
return Metadata.CURRENT_DIRECTORY.relativize(given).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
@@ -95,17 +97,22 @@ public class FileItem extends BorderPane {
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
if (path.get() != null) {
File file = new File(path.get());
if (file.exists()) {
if (file.isFile())
file = file.getAbsoluteFile().getParentFile();
else if (file.isDirectory())
file = file.getAbsoluteFile();
chooser.setInitialDirectory(file);
Path file;
try {
file = Path.of(path.get());
if (Files.exists(file)) {
if (Files.isRegularFile(file))
file = file.toAbsolutePath().normalize().getParent();
else if (Files.isDirectory(file))
file = file.toAbsolutePath().normalize();
chooser.setInitialDirectory(file.toFile());
}
} catch (InvalidPathException e) {
LOG.warning("Failed to resolve path: " + path.get());
}
}
chooser.titleProperty().bind(titleProperty());
File selectedDir = chooser.showDialog(Controllers.getStage());
var selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null) {
path.set(processPath(selectedDir.toString()));
}
@@ -31,8 +31,9 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -86,9 +87,9 @@ public class FileSelector extends HBox {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
File dir = chooser.showDialog(Controllers.getStage());
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
if (dir != null) {
String path = dir.getAbsolutePath();
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
value.setValue(path);
}
@@ -96,9 +97,9 @@ public class FileSelector extends HBox {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
File file = chooser.showOpenDialog(Controllers.getStage());
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) {
String path = file.getAbsolutePath();
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
value.setValue(path);
}
@@ -25,6 +25,8 @@ import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -61,7 +63,12 @@ public class OptionToggleButton extends StackPane {
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
container.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY) {
toggleButton.setSelected(!toggleButton.isSelected());
e.consume();
}
});
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
if (StringUtils.isNotBlank(subtitle)) {
@@ -191,7 +191,7 @@ public class RipplerContainer extends StackPane {
return StyleableProperties.FACTORY.getCssMetaData();
}
private static class StyleableProperties {
private final static class StyleableProperties {
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
@@ -225,7 +225,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
}
}
private class HeadersRegion extends StackPane {
private final class HeadersRegion extends StackPane {
private SideAction action;
private final ObjectProperty<Side> side = new SimpleObjectProperty<Side>() {
@Override
@@ -379,7 +379,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
}
}
private class Top extends Horizontal {
private final class Top extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
@@ -404,7 +404,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
}
}
private class Bottom extends Horizontal {
private final class Bottom extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
@@ -519,7 +519,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
}
}
private class Left extends Vertical {
private final class Left extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
@@ -543,7 +543,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
}
}
private class Right extends Vertical {
private final class Right extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
@@ -40,6 +40,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor;
private TaskCancellationAction onCancel;
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
private final Label lblTitle;
@@ -84,7 +85,7 @@ public class TaskExecutorDialogPane extends BorderPane {
}
});
speedEventHandler = speedEvent -> {
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
String unit = "B/s";
double speed = speedEvent.getSpeed();
if (speed > 1024) {
@@ -95,11 +96,10 @@ public class TaskExecutorDialogPane extends BorderPane {
speed /= 1024;
unit = "MiB/s";
}
double finalSpeed = speed;
String finalUnit = unit;
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
};
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
String message = String.format("%.1f %s", speed, unit);
Platform.runLater(() -> lblProgress.setText(message));
});
onEscPressed(this, btnCancel::fire);
}
@@ -23,8 +23,8 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.SettingsMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
@@ -94,7 +94,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
}
@Override
public void handleTask(Map<String, Object> settings, Task<?> task) {
public void handleTask(SettingsMap settings, Task<?> task) {
displayer.handleTask(settings, task);
}
@@ -35,8 +35,7 @@ import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -80,12 +79,12 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
protected abstract void reload();
@Override
public void onNavigate(Map<String, Object> settings) {
public void onNavigate(SettingsMap settings) {
reload();
}
@Override
public abstract void cleanup(Map<String, Object> settings);
public abstract void cleanup(SettingsMap settings);
protected abstract void onInstall();
@@ -120,20 +119,15 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
{
InstallerItem[] libraries = control.group.getLibraries();
FlowPane libraryPane = new FlowPane(libraries);
libraryPane.setVgap(16);
libraryPane.setHgap(16);
FlowPane libraryPane = new FlowPane(16, 16, libraries);
ScrollPane scrollPane = new ScrollPane(libraryPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
root.setCenter(scrollPane);
if (libraries.length <= 8) {
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
root.setCenter(libraryPane);
} else {
ScrollPane scrollPane = new ScrollPane(libraryPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
root.setCenter(scrollPane);
}
if (libraries.length <= 8)
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
}
{
@@ -29,8 +29,8 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.SettingsMap;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
@@ -109,6 +109,6 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
}
}
@@ -51,14 +51,13 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.function.Supplier;
@@ -145,10 +144,10 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
if (!OperatingSystem.isNameValid(result)) {
if (!FileUtils.isNameValid(result)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}
@@ -223,7 +222,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
private static final class DownloadNavigator implements Navigation {
private final Map<String, Object> settings = new HashMap<>();
private final SettingsMap settings = new SettingsMap();
@Override
public void onStart() {
@@ -260,7 +259,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
@Override
public Map<String, Object> getSettings() {
public SettingsMap getSettings() {
return settings;
}
@@ -287,37 +286,39 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
@Override
public void start(Map<String, Object> settings) {
settings.put(PROFILE, profile);
public void start(SettingsMap settings) {
settings.put(ModpackPage.PROFILE, profile);
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion);
}
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
GameBuilder builder = dependencyManager.gameBuilder();
String name = (String) settings.get("name");
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())).getGameVersion());
for (Map.Entry<String, Object> entry : settings.entrySet())
if (!LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(entry.getKey()) && entry.getValue() instanceof RemoteVersion)
builder.version((RemoteVersion) entry.getValue());
settings.asStringMap().forEach((key, value) -> {
if (!LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(key)
&& value instanceof RemoteVersion remoteVersion)
builder.version(remoteVersion);
});
return builder.buildAsync().whenComplete(any -> profile.getRepository().refreshVersions())
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
}
@Override
public Object finish(Map<String, Object> settings) {
public Object finish(SettingsMap settings) {
settings.put("title", i18n("install.new_game.installation"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
return finishVersionDownloadingAsync(settings);
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
@@ -330,7 +331,5 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
public boolean cancel() {
return true;
}
public static final String PROFILE = "PROFILE";
}
}
@@ -27,8 +27,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -73,7 +72,7 @@ public class InstallersPage extends AbstractInstallersPage {
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
}
private static boolean checkName(String name) {
@@ -36,16 +36,14 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Optional;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -58,11 +56,11 @@ public final class LocalModpackPage extends ModpackPage {
public LocalModpackPage(WizardController controller) {
super(controller);
Profile profile = (Profile) controller.getSettings().get("PROFILE");
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
if (name.isPresent()) {
txtModpackName.setText(name.get());
String name = controller.getSettings().get(MODPACK_NAME);
if (name != null) {
txtModpackName.setText(name);
txtModpackName.setDisable(true);
} else {
FXUtils.onChangeAndOperate(installAsVersion, installAsVersion -> {
@@ -82,15 +80,15 @@ public final class LocalModpackPage extends ModpackPage {
btnDescription.setVisible(false);
File selectedFile;
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
selectedFile = filePath.get();
Path selectedFile;
Path filePath = controller.getSettings().get(MODPACK_FILE);
if (filePath != null) {
selectedFile = filePath;
} else {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
selectedFile = chooser.showOpenDialog(Controllers.getStage());
selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (selectedFile == null) {
controller.onEnd();
return;
@@ -100,19 +98,19 @@ public final class LocalModpackPage extends ModpackPage {
}
showSpinner();
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile))
.thenApplyAsync(encoding -> {
charset = encoding;
manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding);
manifest = ModpackHelper.readModpackManifest(selectedFile, encoding);
return manifest;
})
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
if (exception instanceof ManuallyCreatedModpackException) {
hideSpinner();
lblName.setText(selectedFile.getName());
lblName.setText(FileUtils.getName(selectedFile));
installAsVersion.set(false);
if (!name.isPresent()) {
if (name == null) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
}
@@ -133,7 +131,7 @@ public final class LocalModpackPage extends ModpackPage {
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
if (!name.isPresent()) {
if (name == null) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
}
@@ -144,7 +142,7 @@ public final class LocalModpackPage extends ModpackPage {
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
settings.remove(MODPACK_FILE);
}
@@ -177,9 +175,9 @@ public final class LocalModpackPage extends ModpackPage {
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
public static final String MODPACK_CHARSET = "MODPACK_CHARSET";
public static final String MODPACK_MANUALLY_CREATED = "MODPACK_MANUALLY_CREATED";
public static final SettingsMap.Key<Path> MODPACK_FILE = new SettingsMap.Key<>("MODPACK_FILE");
public static final SettingsMap.Key<String> MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
public static final SettingsMap.Key<Modpack> MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
public static final SettingsMap.Key<Boolean> MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
}
@@ -32,26 +32,25 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.SettingsMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModpackInstallWizardProvider implements WizardProvider {
public final class ModpackInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final File file;
private final Path file;
private final String updateVersion;
public ModpackInstallWizardProvider(Profile profile) {
this(profile, null, null);
}
public ModpackInstallWizardProvider(Profile profile, File modpackFile) {
public ModpackInstallWizardProvider(Profile profile, Path modpackFile) {
this(profile, modpackFile, null);
}
@@ -59,28 +58,28 @@ public class ModpackInstallWizardProvider implements WizardProvider {
this(profile, null, updateVersion);
}
public ModpackInstallWizardProvider(Profile profile, File modpackFile, String updateVersion) {
public ModpackInstallWizardProvider(Profile profile, Path modpackFile, String updateVersion) {
this.profile = profile;
this.file = modpackFile;
this.updateVersion = updateVersion;
}
@Override
public void start(Map<String, Object> settings) {
public void start(SettingsMap settings) {
if (file != null)
settings.put(LocalModpackPage.MODPACK_FILE, file);
if (updateVersion != null)
settings.put(LocalModpackPage.MODPACK_NAME, updateVersion);
settings.put(PROFILE, profile);
settings.put(ModpackPage.PROFILE, profile);
}
private Task<?> finishModpackInstallingAsync(Map<String, Object> settings) {
File selected = tryCast(settings.get(LocalModpackPage.MODPACK_FILE), File.class).orElse(null);
ServerModpackManifest serverModpackManifest = tryCast(settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST), ServerModpackManifest.class).orElse(null);
Modpack modpack = tryCast(settings.get(LocalModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(LocalModpackPage.MODPACK_NAME), String.class).orElse(null);
Charset charset = tryCast(settings.get(LocalModpackPage.MODPACK_CHARSET), Charset.class).orElse(null);
boolean isManuallyCreated = tryCast(settings.get(LocalModpackPage.MODPACK_MANUALLY_CREATED), Boolean.class).orElse(false);
private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
Path selected = settings.get(LocalModpackPage.MODPACK_FILE);
ServerModpackManifest serverModpackManifest = settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST);
Modpack modpack = settings.get(LocalModpackPage.MODPACK_MANIFEST);
String name = settings.get(LocalModpackPage.MODPACK_NAME);
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
if (isManuallyCreated) {
return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset);
@@ -119,21 +118,18 @@ public class ModpackInstallWizardProvider implements WizardProvider {
}
@Override
public Object finish(Map<String, Object> settings) {
public Object finish(SettingsMap settings) {
settings.put("title", i18n("install.modpack.installation"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", new FailureCallback() {
@Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof ModpackCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.SUCCESS, next);
}
settings.put(FailureCallback.KEY, (ignored, exception, next) -> {
if (exception instanceof ModpackCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else {
UpdateInstallerWizardProvider.alertFailureMessage(exception, next);
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.SUCCESS, next);
}
} else {
UpdateInstallerWizardProvider.alertFailureMessage(exception, next);
}
});
@@ -141,7 +137,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new ModpackSelectionPage(controller);
@@ -161,6 +157,4 @@ public class ModpackInstallWizardProvider implements WizardProvider {
public boolean cancel() {
return true;
}
public static final String PROFILE = "PROFILE";
}
@@ -7,16 +7,20 @@ import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class ModpackPage extends SpinnerPane implements WizardPage {
public static final SettingsMap.Key<Profile> PROFILE = new SettingsMap.Key<>("PROFILE");
protected final WizardController controller;
protected final Label lblName;
@@ -78,8 +82,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
BorderPane descriptionPane = new BorderPane();
{
btnDescription = new JFXButton(i18n("modpack.description"));
btnDescription.getStyleClass().add("jfx-button-border");
btnDescription = FXUtils.newBorderButton(i18n("modpack.description"));
btnDescription.setOnAction(e -> onDescribe());
descriptionPane.setLeft(btnDescription);
@@ -36,20 +36,18 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_FILE;
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_NAME;
import static org.jackhuang.hmcl.ui.download.RemoteModpackPage.MODPACK_SERVER_MANIFEST;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackSelectionPage extends VBox implements WizardPage {
@@ -71,14 +69,14 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
createButton("repository", this::onChooseRepository)
);
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
controller.getSettings().put(MODPACK_FILE, filePath.get());
Path filePath = controller.getSettings().get(MODPACK_FILE);
if (filePath != null) {
controller.getSettings().put(MODPACK_FILE, filePath);
Platform.runLater(controller::onNext);
}
FXUtils.applyDragListener(this, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Path modpack = modpacks.get(0);
controller.getSettings().put(MODPACK_FILE, modpack);
controller.onNext();
});
@@ -112,7 +110,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
return;
@@ -150,7 +148,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
resolve.run();
controller.getSettings().put(MODPACK_FILE, modpack.toFile());
controller.getSettings().put(MODPACK_FILE, modpack);
controller.onNext();
} else {
reject.accept(e.getMessage());
@@ -167,14 +165,14 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
}
public void onChooseRepository() {
String modPackName = (String) controller.getSettings().get(MODPACK_NAME);
String modPackName = controller.getSettings().get(MODPACK_NAME);
DownloadPage downloadPage = new DownloadPage(modPackName);
downloadPage.showModpackDownloads();
Controllers.navigate(downloadPage);
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
}
@Override
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.application.Platform;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
@@ -27,13 +28,11 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class RemoteModpackPage extends ModpackPage {
@@ -42,8 +41,9 @@ public final class RemoteModpackPage extends ModpackPage {
public RemoteModpackPage(WizardController controller) {
super(controller);
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
manifest = controller.getSettings().get(MODPACK_SERVER_MANIFEST);
if (manifest == null)
throw new IllegalStateException("MODPACK_SERVER_MANIFEST should exist");
try {
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack(null));
@@ -57,10 +57,10 @@ public final class RemoteModpackPage extends ModpackPage {
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
Profile profile = (Profile) controller.getSettings().get("PROFILE");
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
if (name.isPresent()) {
txtModpackName.setText(name.get());
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
String name = controller.getSettings().get(MODPACK_NAME);
if (name != null) {
txtModpackName.setText(name);
txtModpackName.setDisable(true);
} else {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
@@ -75,7 +75,7 @@ public final class RemoteModpackPage extends ModpackPage {
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
settings.remove(MODPACK_SERVER_MANIFEST);
}
@@ -89,7 +89,7 @@ public final class RemoteModpackPage extends ModpackPage {
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
public static final SettingsMap.Key<ServerModpackManifest> MODPACK_SERVER_MANIFEST = new SettingsMap.Key<>("MODPACK_SERVER_MANIFEST");
public static final SettingsMap.Key<String> MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
public static final SettingsMap.Key<Modpack> MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
@@ -40,7 +41,6 @@ import java.net.SocketTimeoutException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.zip.ZipException;
@@ -66,30 +66,29 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
}
@Override
public void start(Map<String, Object> settings) {
public void start(SettingsMap settings) {
}
@Override
public Object finish(Map<String, Object> settings) {
public Object finish(SettingsMap settings) {
settings.put("title", i18n("install.change_version.process"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
settings.put(FailureCallback.KEY, (settings1, exception, next) -> alertFailureMessage(exception, next));
// We remove library but not save it,
// so if installation failed will not break down current version.
Task<Version> ret = Task.supplyAsync(() -> version);
List<String> stages = new ArrayList<>();
for (Object value : settings.values()) {
if (value instanceof RemoteVersion) {
RemoteVersion remoteVersion = (RemoteVersion) value;
for (Object value : settings.asStringMap().values()) {
if (value instanceof RemoteVersion remoteVersion) {
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
if ("game".equals(remoteVersion.getLibraryId())) {
stages.add("hmcl.install.libraries");
stages.add("hmcl.install.assets");
}
} else if (value instanceof RemoveVersionAction) {
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, ((RemoveVersionAction) value).libraryId));
} else if (value instanceof RemoveVersionAction removeVersionAction) {
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, removeVersionAction.libraryId));
}
}
@@ -97,7 +96,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> {
@@ -28,8 +28,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -45,36 +44,37 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
}
@Override
public void start(Map<String, Object> settings) {
settings.put(PROFILE, profile);
public void start(SettingsMap settings) {
settings.put(ModpackPage.PROFILE, profile);
}
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
GameBuilder builder = dependencyManager.gameBuilder();
String name = (String) settings.get("name");
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
for (Map.Entry<String, Object> entry : settings.entrySet())
if (!"game".equals(entry.getKey()) && entry.getValue() instanceof RemoteVersion)
builder.version((RemoteVersion) entry.getValue());
settings.asStringMap().forEach((key, value) -> {
if (!"game".equals(key) && value instanceof RemoteVersion remoteVersion)
builder.version(remoteVersion);
});
return builder.buildAsync().whenComplete(any -> profile.getRepository().refreshVersions())
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
}
@Override
public Object finish(Map<String, Object> settings) {
public Object finish(SettingsMap settings) {
settings.put("title", i18n("install.new_game.installation"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
return finishVersionDownloadingAsync(settings);
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
@@ -88,6 +88,4 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
public boolean cancel() {
return true;
}
public static final String PROFILE = "PROFILE";
}
@@ -54,12 +54,12 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -124,7 +124,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
settings.remove(libraryId);
}
@@ -35,10 +35,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -55,26 +55,25 @@ public final class ExportWizardProvider implements WizardProvider {
}
@Override
public void start(Map<String, Object> settings) {
public void start(SettingsMap settings) {
}
@Override
public Object finish(Map<String, Object> settings) {
@SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
public Object finish(SettingsMap settings) {
List<String> whitelist = settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
Path modpackFile = settings.get(ModpackInfoPage.MODPACK_FILE);
ModpackExportInfo exportInfo = settings.get(ModpackInfoPage.MODPACK_INFO);
exportInfo.setWhitelist(whitelist);
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
String modpackType = settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
return exportWithLauncher(modpackType, exportInfo, modpackFile);
}
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, Path modpackFile) {
Path launcherJar = JarUtils.thisJarPath();
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
return new Task<Object>() {
File tempModpack;
return new Task<>() {
Path tempModpack;
Task<?> exportTask;
{
@@ -88,9 +87,9 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void preExecute() throws Exception {
File dest;
Path dest;
if (packWithLauncher) {
dest = tempModpack = Files.createTempFile("hmcl", ".zip").toFile();
dest = tempModpack = Files.createTempFile("hmcl", ".zip");
} else {
dest = modpackFile;
}
@@ -122,7 +121,7 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void execute() throws Exception {
if (!packWithLauncher) return;
try (Zipper zip = new Zipper(modpackFile.toPath())) {
try (Zipper zip = new Zipper(modpackFile)) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
@@ -165,7 +164,7 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, Path modpackFile) {
return new Task<Void>() {
Task<?> dependency = null;
@@ -185,7 +184,7 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, Path modpackFile) {
return new Task<Void>() {
Task<?> dependency;
@@ -233,7 +232,7 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsServer(ModpackExportInfo exportInfo, Path modpackFile) {
return new Task<Void>() {
Task<?> dependency;
@@ -253,7 +252,7 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, Path modpackFile) {
return new Task<Void>() {
Task<?> dependency;
@@ -279,17 +278,13 @@ public final class ExportWizardProvider implements WizardProvider {
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
switch (step) {
case 0:
return new ModpackTypeSelectionPage(controller);
case 1:
return new ModpackInfoPage(controller, profile.getRepository(), version);
case 2:
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
default:
throw new IllegalArgumentException("step");
}
public Node createPage(WizardController controller, int step, SettingsMap settings) {
return switch (step) {
case 0 -> new ModpackTypeSelectionPage(controller);
case 1 -> new ModpackInfoPage(controller, profile.getRepository(), version);
case 2 -> new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
default -> throw new IllegalArgumentException("step");
};
}
@Override
@@ -33,10 +33,13 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -45,6 +48,7 @@ import java.util.Objects;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
@@ -80,16 +84,18 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
this.setBottom(nextPane);
}
private CheckBoxTreeItem<String> getTreeItem(File file, String basePath) {
if (!file.exists())
private CheckBoxTreeItem<String> getTreeItem(Path file, String basePath) {
if (Files.notExists(file))
return null;
boolean isDirectory = Files.isDirectory(file);
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
if (basePath.length() > "minecraft/".length()) {
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (file.isDirectory() ? "/" : ""), file.isDirectory());
if (file.isFile() && Objects.equals(FileUtils.getNameWithoutExtension(file), version)) // Ignore <version>.json, <version>.jar
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (isDirectory ? "/" : ""), isDirectory);
if (!isDirectory && Objects.equals(FileUtils.getNameWithoutExtension(file), version))
state = ModAdviser.ModSuggestion.HIDDEN;
if (file.isDirectory() && Objects.equals(file.getName(), version + "-natives")) // Ignore <version>-natives
if (isDirectory && Objects.equals(FileUtils.getName(file), version + "-natives")) // Ignore <version>-natives
state = ModAdviser.ModSuggestion.HIDDEN;
if (state == ModAdviser.ModSuggestion.HIDDEN)
return null;
@@ -99,11 +105,10 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
if (state == ModAdviser.ModSuggestion.SUGGESTED)
node.setSelected(true);
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File it : files) {
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + it.getName());
if (isDirectory) {
try (var stream = Files.list(file)) {
stream.forEach(it -> {
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + FileUtils.getName(it));
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected()) {
@@ -111,8 +116,11 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
}
node.getChildren().add(subNode);
}
}
});
} catch (IOException e) {
LOG.warning("Failed to list contents of " + file, e);
}
if (!node.isSelected()) node.setIndeterminate(false);
// Empty folder need not to be displayed.
@@ -155,7 +163,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
controller.getSettings().remove(MODPACK_FILE_SELECTION);
}
@@ -171,7 +179,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
return i18n("modpack.wizard.step.2.title");
}
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
public static final SettingsMap.Key<List<String>> MODPACK_FILE_SELECTION = new SettingsMap.Key<>("modpack.accepted");
private static final Map<String, String> TRANSLATION = mapOf(
pair("minecraft/hmclversion.cfg", i18n("modpack.files.hmclversion_cfg")),
pair("minecraft/servers.dat", i18n("modpack.files.servers_dat")),
@@ -41,11 +41,13 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -56,7 +58,6 @@ import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackInfoPage extends Control implements WizardPage {
@@ -87,10 +88,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller;
this.gameRepository = gameRepository;
this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
.orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
this.options = controller.getSettings().get(MODPACK_INFO_OPTION);
this.versionName = version;
if (this.options == null)
throw new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required");
name.set(version);
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
@@ -110,9 +113,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
fileChooser.setInitialFileName(name.get() + ".mrpack");
} else {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
fileChooser.setInitialFileName(name.get() + ".zip");
fileChooser.setInitialFileName(name.get() + ".zip");
}
File file = fileChooser.showSaveDialog(Controllers.getStage());
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
if (file == null) {
controller.onEnd();
return;
@@ -146,7 +149,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
controller.getSettings().remove(MODPACK_INFO);
}
@@ -160,9 +163,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
return new ModpackInfoPageSkin(this);
}
public static final String MODPACK_INFO = "modpack.info";
public static final String MODPACK_FILE = "modpack.file";
public static final String MODPACK_INFO_OPTION = "modpack.info.option";
public static final SettingsMap.Key<ModpackExportInfo> MODPACK_INFO = new SettingsMap.Key<>("modpack.info");
public static final SettingsMap.Key<Path> MODPACK_FILE = new SettingsMap.Key<>("modpack.file");
public static final SettingsMap.Key<ModpackExportInfo.Options> MODPACK_INFO_OPTION = new SettingsMap.Key<>("modpack.info.option");
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private ObservableList<Node> originList;
@@ -189,7 +192,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
hyperlink.setOnAction(e -> FXUtils.openLink(Metadata.DOCS_URL + "/modpack/serverpack.html"));
borderPane.setTop(hyperlink);
} if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
}
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
pane.setText(i18n("modpack.wizard.step.initialization.modrinth.info"));
borderPane.setTop(pane);
@@ -381,7 +385,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
button.setMaxHeight(16);
pane.setRight(button);
}
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
BorderPane noCreateRemoteFiles = new BorderPane();
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
@@ -33,8 +33,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -89,7 +88,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
}
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
}
@Override
@@ -97,7 +96,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
return i18n("modpack.wizard.step.3.title");
}
public static final String MODPACK_TYPE = "modpack.type";
public static final SettingsMap.Key<String> MODPACK_TYPE = new SettingsMap.Key<>("modpack.type");
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
public static final String MODPACK_TYPE_MULTIMC = "multimc";
@@ -287,7 +287,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
* the mark position becomes invalid.
* @see InputStream#reset()
*/
public synchronized void mark(int readlimit) {
public void mark(int readlimit) {
}
/**
@@ -302,7 +302,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
* @see InputStream#mark(int)
* @see IOException
*/
public synchronized void reset() throws IOException {
public void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
}
@@ -52,8 +52,9 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
@@ -340,8 +341,8 @@ public final class JavaDownloadDialog extends StackPane {
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
throw new IOException("Missing download URI: " + json);
File targetFile = File.createTempFile("hmcl-java-", "." + version.getArchiveType());
targetFile.deleteOnExit();
Path targetFile = Files.createTempFile("hmcl-java-", "." + version.getArchiveType());
targetFile.toFile().deleteOnExit();
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
@@ -363,8 +364,8 @@ public final class JavaDownloadDialog extends StackPane {
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile.toPath(), integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath);
targetFile, integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(() -> targetFile);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
@@ -45,13 +45,14 @@ import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -79,19 +80,19 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
}
FXUtils.applyDragListener(this, it -> {
String name = it.getName();
return it.isDirectory() || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
String name = FileUtils.getName(it);
return Files.isDirectory(it) || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
}, files -> {
for (File file : files) {
if (file.isDirectory()) {
onAddJavaHome(file.toPath());
for (Path file : files) {
if (Files.isDirectory(file)) {
onAddJavaHome(file);
} else {
String fileName = file.getName();
String fileName = FileUtils.getName(file);
if (fileName.equals(OperatingSystem.CURRENT_OS.getJavaExecutable())) {
onAddJavaBinary(file.toPath());
onAddJavaBinary(file);
} else if (fileName.endsWith(".zip") || fileName.endsWith(".tar.gz")) {
onInstallArchive(file.toPath());
onInstallArchive(file);
} else {
throw new AssertionError("Unreachable code");
}
@@ -110,9 +111,9 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
chooser.setTitle(i18n("settings.game.java_directory.choose"));
File file = chooser.showOpenDialog(Controllers.getStage());
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) {
JavaManager.getAddJavaTask(file.toPath()).whenComplete(Schedulers.javafx(), exception -> {
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
@@ -72,6 +72,7 @@ import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -115,6 +116,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
ImageView titleIcon = new ImageView(FXUtils.newBuiltinImage("/assets/img/icon-title.png"));
Label titleLabel = new Label(Metadata.FULL_TITLE);
if (I18n.isUpsideDown()) {
titleIcon.setRotate(180);
titleLabel.setRotate(180);
}
titleLabel.getStyleClass().add("jfx-decorator-title");
titleNode.getChildren().setAll(titleIcon, titleLabel);
@@ -208,20 +213,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchPane.getStyleClass().add("launch-pane");
launchPane.setMaxWidth(230);
launchPane.setMaxHeight(55);
launchPane.setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
FXUtils.onScroll(launchPane, versions, list -> {
String currentId = getCurrentGame();
int index = Lang.indexWhere(versions, instance -> instance.getId().equals(currentId));
if (index < 0) return;
if (deltaY > 0) // up
index--;
else // down
index++;
profile.setSelectedVersion(versions.get((index + versions.size()) % versions.size()).getId());
});
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, it -> profile.setSelectedVersion(it.getId()));
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
{
JFXButton launchButton = new JFXButton();
@@ -435,6 +431,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
return state;
}
public Profile getProfile() {
return profile;
}
public String getCurrentGame() {
return currentGame.get();
}
@@ -447,6 +447,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
this.currentGame.set(currentGame);
}
public ObservableList<Version> getVersions() {
return versions;
}
public boolean isShowUpdate() {
return showUpdate.get();
}
@@ -174,6 +174,7 @@ public class PersonalizationPage extends StackPane {
}
Label textOpacity = new Label();
FXUtils.setLimitWidth(textOpacity, 35);
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
textOpacity.textProperty().bind(valueBinding);
@@ -51,7 +51,8 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
@@ -92,16 +93,16 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage,
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file.toPath()),
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
modpacks -> {
File file = modpacks.get(0);
Path file = modpacks.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file),
i18n("install.modpack"));
} else if (NBTFileType.isNBTFileByExtension(file.toPath())) {
} else if (NBTFileType.isNBTFileByExtension(file)) {
try {
Controllers.navigate(new NBTEditorPage(file.toPath()));
Controllers.navigate(new NBTEditorPage(file));
} catch (Throwable e) {
LOG.warning("Fail to open nbt file", e);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
@@ -153,6 +154,10 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Versions.modifyGameSettings(profile, version);
}
});
FXUtils.onScroll(gameListItem, getSkinnable().getMainPage().getVersions(), list -> {
String currentId = getSkinnable().getMainPage().getCurrentGame();
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, it -> getSkinnable().getMainPage().getProfile().setSelectedVersion(it.getId()));
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
@@ -193,8 +198,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
.add(downloadItem)
.startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT))
.add(launcherSettingsItem)
.add(chatItem)
;
.add(chatItem);
// the root page, with the sidebar in left, navigator in center.
setLeft(sideBar);
@@ -211,14 +215,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
checkedModpack = true;
if (repository.getVersionCount() == 0) {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
Path modpackFile = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
if (Files.exists(modpackFile)) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
.thenApplyAsync(
encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
.thenApplyAsync(modpack -> ModpackHelper
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(),
modpack)
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.executor())
.thenAcceptAsync(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
@@ -33,12 +33,11 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.tukaani.xz.XZInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -66,7 +65,7 @@ public final class SettingsPage extends SettingsView {
FXUtils.smoothScrolling(scroll);
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
@@ -254,6 +253,9 @@ public final class SettingsPage extends SettingsView {
@Override
protected void clearCacheDirectory() {
FileUtils.cleanDirectoryQuietly(new File(Settings.instance().getCommonDirectory(), "cache"));
String commonDirectory = Settings.instance().getCommonDirectory();
if (commonDirectory != null) {
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
}
}
}
@@ -39,7 +39,7 @@ import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import java.util.Arrays;
@@ -162,9 +162,8 @@ public abstract class SettingsView extends StackPane {
));
{
JFXButton cleanButton = new JFXButton(i18n("launcher.cache_directory.clean"));
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
cleanButton.getStyleClass().add("jfx-button-border");
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
@@ -216,9 +215,8 @@ public abstract class SettingsView extends StackPane {
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnAction(e -> onExportLogs());
logButton.getStyleClass().add("jfx-button-border");
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
@@ -236,7 +234,7 @@ public abstract class SettingsView extends StackPane {
}
public void openLogFolder() {
FXUtils.openFolder(LOG.getLogFile().getParent().toFile());
FXUtils.openFolder(LOG.getLogFile().getParent());
}
protected abstract void onUpdate();
@@ -27,13 +27,14 @@ import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.*;
import java.io.IOException;
import java.nio.file.Path;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -68,9 +69,7 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
actions.setPadding(new Insets(8));
actions.setAlignment(Pos.CENTER_RIGHT);
JFXButton saveButton = new JFXButton(i18n("button.save"));
saveButton.getStyleClass().add("jfx-button-raised");
saveButton.setButtonType(JFXButton.ButtonType.RAISED);
JFXButton saveButton = FXUtils.newRaisedButton(i18n("button.save"));
saveButton.setOnAction(e -> {
try {
save();
@@ -80,9 +79,7 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
}
});
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("jfx-button-raised");
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
onEscPressed(this, cancelButton::fire);
@@ -25,7 +25,11 @@ import com.github.steveice10.opennbt.tag.builtin.Tag;
import kala.compress.utils.BoundedInputStream;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
@@ -42,8 +42,9 @@ import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.file.Path;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -68,7 +69,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
state.set(State.fromTitle(profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName));
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(".minecraft"));
Optional.ofNullable(profile).map(Profile::getGameDir).map(FileUtils::getAbsolutePath).orElse(".minecraft"));
ScrollPane scroll = new ScrollPane();
this.setCenter(scroll);
@@ -153,13 +154,13 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
profile.setName(txtProfileName.getText());
profile.setUseRelativePath(toggleUseRelativePath.isSelected());
if (StringUtils.isNotBlank(getLocation())) {
profile.setGameDir(new File(getLocation()));
profile.setGameDir(Path.of(getLocation()));
}
} else {
if (StringUtils.isBlank(getLocation())) {
gameDir.onExplore();
}
Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation()));
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
Profiles.getProfiles().add(newProfile);
}
@@ -43,6 +43,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final JFXTextField txtPreLaunchCommand;
private final JFXTextField txtPostExitCommand;
private final OptionToggleButton noJVMArgsPane;
private final OptionToggleButton noOptimizingJVMArgsPane;
private final OptionToggleButton noGameCheckPane;
private final OptionToggleButton noJVMCheckPane;
private final OptionToggleButton noNativesPatchPane;
@@ -183,6 +184,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
noJVMArgsPane = new OptionToggleButton();
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
noOptimizingJVMArgsPane = new OptionToggleButton();
noOptimizingJVMArgsPane.setTitle(i18n("settings.advanced.no_optimizing_jvm_args"));
noOptimizingJVMArgsPane.disableProperty().bind(noJVMArgsPane.selectedProperty());
noGameCheckPane = new OptionToggleButton();
noGameCheckPane.setTitle(i18n("settings.advanced.dont_check_game_completeness"));
@@ -199,7 +204,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
workaroundPane.getContent().setAll(
nativesDirSublist, rendererPane, noJVMArgsPane, noGameCheckPane,
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
noJVMCheckPane, noNativesPatchPane
);
@@ -235,6 +240,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
noOptimizingJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noOptimizingJVMArgsProperty());
noNativesPatchPane.selectedProperty().bindBidirectional(versionSetting.notPatchNativesProperty());
useNativeGLFWPane.selectedProperty().bindBidirectional(versionSetting.useNativeGLFWProperty());
useNativeOpenALPane.selectedProperty().bindBidirectional(versionSetting.useNativeOpenALProperty());
@@ -245,12 +251,12 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
String nativesDirName = "natives-" + Platform.SYSTEM_PLATFORM;
if (versionId == null) {
return String.format("%s/%s/%s",
profile.getRepository().getBaseDirectory().toPath().resolve("versions").toAbsolutePath().normalize(),
profile.getRepository().getBaseDirectory().resolve("versions").toAbsolutePath().normalize(),
i18n("settings.advanced.natives_directory.default.version_id"),
nativesDirName
);
} else {
return profile.getRepository().getVersionRoot(versionId).toPath()
return profile.getRepository().getVersionRoot(versionId)
.toAbsolutePath().normalize()
.resolve(nativesDirName)
.toString();
@@ -276,6 +282,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
noOptimizingJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noOptimizingJVMArgsProperty());
noNativesPatchPane.selectedProperty().unbindBidirectional(versionSetting.notPatchNativesProperty());
useNativeGLFWPane.selectedProperty().unbindBidirectional(versionSetting.useNativeGLFWProperty());
useNativeOpenALPane.selectedProperty().unbindBidirectional(versionSetting.useNativeOpenALProperty());
@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
@@ -54,9 +53,9 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
}
private void installSingleDatapack(File datapack) {
private void installSingleDatapack(Path datapack) {
try {
Datapack zip = new Datapack(datapack.toPath());
Datapack zip = new Datapack(datapack);
zip.loadFromZip();
zip.installTo(worldDir);
} catch (IOException | IllegalArgumentException e) {
@@ -82,7 +81,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res != null)
res.forEach(this::installSingleDatapack);
@@ -49,11 +49,12 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -172,14 +173,14 @@ public class DownloadPage extends Control implements DecoratorPage {
fileChooser.setTitle(i18n("button.save_as"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension));
fileChooser.setInitialFileName(file.getFile().getFilename());
File dest = fileChooser.showSaveDialog(Controllers.getStage());
Path dest = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
if (dest == null) {
return;
}
Controllers.taskDialog(
Task.composeAsync(() -> {
var task = new FileDownloadTask(file.getFile().getUrl(), dest.toPath(), file.getFile().getIntegrityCheck());
var task = new FileDownloadTask(file.getFile().getUrl(), dest, file.getFile().getIntegrityCheck());
task.setName(file.getName());
return task;
}),
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -137,12 +137,12 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
public void installOffline() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
File file = chooser.showOpenDialog(Controllers.getStage());
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) doInstallOffline(file);
}
private void doInstallOffline(File file) {
Task<?> task = profile.getDependency().installLibraryAsync(version, file.toPath())
private void doInstallOffline(Path file) {
Task<?> task = profile.getDependency().installLibraryAsync(version, file)
.thenComposeAsync(profile.getRepository()::saveAsync)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
task.setName(i18n("install.installer.install_offline"));
@@ -39,9 +39,9 @@ import org.jackhuang.hmcl.ui.construct.PageAware;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@@ -62,7 +62,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
mods.forEach(it -> {
try {
modManager.addMod(it.toPath());
modManager.addMod(it);
} catch (IOException | IllegalArgumentException e) {
LOG.warning("Unable to parse mod file " + it, e);
}
@@ -117,7 +117,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res == null) return;
@@ -126,13 +126,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
List<String> failed = new ArrayList<>();
Task.runAsync(() -> {
for (File file : res) {
for (Path file : res) {
try {
modManager.addMod(file.toPath());
succeeded.add(file.getName());
modManager.addMod(file);
succeeded.add(FileUtils.getName(file));
} catch (Exception e) {
LOG.warning("Unable to add mod " + file, e);
failed.add(file.getName());
failed.add(FileUtils.getName(file));
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
}
@@ -175,7 +175,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void openModFolder() {
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(versionId), "mods"));
FXUtils.openFolder(profile.getRepository().getRunDirectory(versionId).resolve("mods"));
}
public void checkUpdates() {
@@ -42,18 +42,15 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -77,8 +74,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
public SchematicsPage() {
FXUtils.applyDragListener(this,
file -> currentDirectory != null && file.isFile() && file.getName().endsWith(".litematic"),
files -> addFiles(files.stream().map(File::toPath).collect(Collectors.toList()))
file -> currentDirectory != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"),
this::addFiles
);
}
@@ -150,9 +147,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
fileChooser.setTitle(i18n("schematics.add"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
i18n("schematics"), "*.litematic"));
List<File> files = fileChooser.showOpenMultipleDialog(Controllers.getStage());
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
if (files != null && !files.isEmpty()) {
addFiles(files.stream().map(File::toPath).collect(Collectors.toList()));
addFiles(files);
}
}
@@ -170,7 +167,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
return;
}
if (result.contains("/") || result.contains("\\") || !OperatingSystem.isNameValid(result)) {
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
reject.accept(i18n("schematics.create_directory.failed.invalid_name"));
return;
}
@@ -368,13 +365,13 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
@Override
void onReveal() {
FXUtils.openFolder(path.toFile());
FXUtils.openFolder(path);
}
@Override
void onDelete() {
try {
FileUtils.cleanDirectory(path.toFile());
FileUtils.cleanDirectory(path);
Files.deleteIfExists(path);
refresh();
} catch (IOException e) {
@@ -31,9 +31,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -75,7 +76,7 @@ public class VersionIconDialog extends DialogPane {
private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter());
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (selectedFile != null) {
try {
profile.getRepository().setVersionIconFile(versionId, selectedFile);
@@ -45,7 +45,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.util.Optional;
import java.util.function.Supplier;
@@ -158,7 +157,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void onBrowse(String sub) {
FXUtils.openFolder(new File(getProfile().getRepository().getRunDirectory(getVersion()), sub));
FXUtils.openFolder(getProfile().getRepository().getRunDirectory(getVersion()).resolve(sub));
}
private void redownloadAssetIndex() {
@@ -166,14 +165,14 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearLibraries() {
FileUtils.deleteDirectoryQuietly(new File(getProfile().getRepository().getBaseDirectory(), "libraries"));
FileUtils.deleteDirectoryQuietly(getProfile().getRepository().getBaseDirectory().resolve("libraries"));
}
private void clearAssets() {
HMCLGameRepository baseDirectory = getProfile().getRepository();
FileUtils.deleteDirectoryQuietly(new File(baseDirectory.getBaseDirectory(), "assets"));
FileUtils.deleteDirectoryQuietly(baseDirectory.getBaseDirectory().resolve("assets"));
if (version.get() != null) {
FileUtils.deleteDirectoryQuietly(new File(baseDirectory.getRunDirectory(version.get().getVersion()), "resources"));
FileUtils.deleteDirectoryQuietly(baseDirectory.getRunDirectory(version.get().getVersion()).resolve("resources"));
}
}
@@ -56,7 +56,6 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -199,7 +198,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
copyGlobalPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
JFXButton copyAll = new JFXButton(i18n("settings.game.copy_global.copy_all"));
JFXButton copyAll = FXUtils.newBorderButton(i18n("settings.game.copy_global.copy_all"));
copyAll.disableProperty().bind(modpack);
copyGlobalPane.setRight(copyAll);
copyAll.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
@@ -210,7 +209,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
}, null));
copyAll.getStyleClass().add("jfx-button-border");
BorderPane.setAlignment(copyAll, Pos.CENTER_RIGHT);
}
@@ -457,7 +455,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
showAdvancedSettingPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
JFXButton button = new JFXButton(i18n("settings.advanced.modify"));
JFXButton button = FXUtils.newBorderButton(i18n("settings.advanced.modify"));
button.setOnAction(e -> {
if (lastVersionSetting != null) {
if (advancedVersionSettingPage == null)
@@ -466,7 +464,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
Controllers.navigate(advancedVersionSettingPage);
}
});
button.getStyleClass().add("jfx-button-border");
showAdvancedSettingPane.setRight(button);
}
@@ -635,7 +632,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
versionSetting.javaVersionProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> profile.getRepository().getRunDirectory(versionId).toAbsolutePath().normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
lastVersionSetting = versionSetting;
@@ -40,10 +40,10 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
@@ -88,9 +88,9 @@ public final class Versions {
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
if (version != null) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack.toFile(), version));
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack, version));
} else {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack.toFile()));
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack));
}
} else if (e instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
@@ -119,7 +119,7 @@ public final class Versions {
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> {
if (!OperatingSystem.isNameValid(newName)) {
if (!FileUtils.isNameValid(newName)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}
@@ -192,14 +192,14 @@ public final class Versions {
ensureSelectedAccount(account -> {
GameRepository repository = profile.getRepository();
FileChooser chooser = new FileChooser();
if (repository.getRunDirectory(id).isDirectory())
chooser.setInitialDirectory(repository.getRunDirectory(id));
if (Files.isDirectory(repository.getRunDirectory(id)))
chooser.setInitialDirectory(repository.getRunDirectory(id).toFile());
chooser.setTitle(i18n("version.launch_script.save"));
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
File file = chooser.showSaveDialog(Controllers.getStage());
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
if (file != null)
new LauncherHelper(profile, account, id).makeLaunchScript(file);
});
@@ -147,7 +147,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
WorldBackupsPage.this.getItems().sort(Comparator.naturalOrder());
Controllers.dialog(i18n("world.backup.create.success", result.getKey()), null, MessageDialogPane.MessageType.INFO);
} else if (exception instanceof WorldLockedException) {
Controllers.dialog(i18n("world.backup.create.locked"), null, MessageDialogPane.MessageType.WARNING);
Controllers.dialog(i18n("world.locked.failed"), null, MessageDialogPane.MessageType.WARNING);
} else {
LOG.warning("Failed to create backup", exception);
Controllers.dialog(i18n("world.backup.create.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING);
@@ -21,11 +21,16 @@ import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -33,10 +38,12 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class WorldListItem extends Control {
private final World world;
private final Path backupsDir;
private final WorldListPage parent;
public WorldListItem(World world, Path backupsDir) {
public WorldListItem(WorldListPage parent, World world, Path backupsDir) {
this.world = world;
this.backupsDir = backupsDir;
this.parent = parent;
}
@Override
@@ -53,16 +60,34 @@ public final class WorldListItem extends Control {
fileChooser.setTitle(i18n("world.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
fileChooser.setInitialFileName(world.getWorldName());
File file = fileChooser.showSaveDialog(Controllers.getStage());
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
if (file == null) {
return;
}
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file.toPath(), controller::onFinish)));
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file, controller::onFinish)));
}
public void delete() {
Controllers.confirm(
i18n("button.remove.confirm"),
i18n("world.delete"),
() -> Task.runAsync(world::delete)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
parent.remove(this);
} else if (exception instanceof WorldLockedException) {
Controllers.dialog(i18n("world.locked.failed"), null, MessageType.WARNING);
} else {
Controllers.dialog(i18n("world.delete.failed", StringUtils.getStackTrace(exception)), null, MessageType.WARNING);
}
}).start(),
null
);
}
public void reveal() {
FXUtils.openFolder(world.getFile().toFile());
FXUtils.openFolder(world.getFile());
}
public void showManagePage() {
@@ -136,6 +136,7 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup),
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup));
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.InvalidPathException;
@@ -39,8 +38,8 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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 WorldListPage extends ListPageBase<WorldListItem> implements VersionPage.VersionLoadable {
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
@@ -61,7 +60,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
if (worlds != null)
itemsProperty().setAll(worlds.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(world, backupsDir)).collect(Collectors.toList()));
.map(world -> new WorldListItem(this, world, backupsDir)).toList());
});
}
@@ -79,6 +78,10 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
refresh();
}
public void remove(WorldListItem item) {
itemsProperty().remove(item);
}
public void refresh() {
if (profile == null || id == null)
return;
@@ -94,8 +97,10 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
worlds = result;
setLoading(false);
if (exception == null) {
itemsProperty().setAll(result.stream().filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(world, backupsDir)).collect(Collectors.toList()));
itemsProperty().setAll(result.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir))
.collect(Collectors.toList()));
} else {
LOG.warning("Failed to load world list page", exception);
}
@@ -106,7 +111,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.import.choose"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res == null || res.isEmpty()) return;
installWorld(res.get(0));
@@ -117,15 +122,15 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
Controllers.navigate(Controllers.getDownloadPage());
}
private void installWorld(File zipFile) {
private void installWorld(Path zipFile) {
// Only accept one world file because user is required to confirm the new world name
// Or too many input dialogs are popped.
Task.supplyAsync(() -> new World(zipFile.toPath()))
Task.supplyAsync(() -> new World(zipFile))
.whenComplete(Schedulers.javafx(), world -> {
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.runAsync(() -> world.install(savesDir, name))
.whenComplete(Schedulers.javafx(), () -> {
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name)), backupsDir));
itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir));
resolve.run();
}, e -> {
if (e instanceof FileAlreadyExistsException)
@@ -117,7 +117,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
chunkBaseMenuItem.getWidth(), 0)));
}
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile().toFile()), null);
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null);
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
left.setBottom(toolbar);
@@ -22,8 +22,8 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.TaskListPane;
import org.jackhuang.hmcl.util.SettingsMap;
import java.util.Map;
import java.util.Queue;
public abstract class AbstractWizardDisplayer implements WizardDisplayer {
@@ -34,7 +34,7 @@ public abstract class AbstractWizardDisplayer implements WizardDisplayer {
}
@Override
public void handleTask(Map<String, Object> settings, Task<?> task) {
public void handleTask(SettingsMap settings, Task<?> task) {
TaskExecutor executor = task.withRunAsync(Schedulers.javafx(), this::navigateToSuccess).executor();
TaskListPane pane = new TaskListPane();
pane.setExecutor(executor);
@@ -18,8 +18,7 @@
package org.jackhuang.hmcl.ui.wizard;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
public interface Navigation {
@@ -37,7 +36,7 @@ public interface Navigation {
void onCancel();
Map<String, Object> getSettings();
SettingsMap getSettings();
enum NavigationDirection {
START(ContainerAnimations.NONE),
@@ -18,8 +18,8 @@
package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.util.SettingsMap;
import java.util.Map;
import java.util.function.Function;
public class SinglePageWizardProvider implements WizardProvider {
@@ -32,16 +32,16 @@ public class SinglePageWizardProvider implements WizardProvider {
}
@Override
public void start(Map<String, Object> settings) {
public void start(SettingsMap settings) {
}
@Override
public Object finish(Map<String, Object> settings) {
public Object finish(SettingsMap settings) {
return page.finish();
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
public Node createPage(WizardController controller, int step, SettingsMap settings) {
if (step != 0) throw new IllegalStateException("Step must be 0");
return page = provider.apply(controller);
@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CancellationException;
@@ -42,7 +42,7 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
}
@Override
public void handleTask(Map<String, Object> settings, Task<?> task) {
public void handleTask(SettingsMap settings, Task<?> task) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(new TaskCancellationAction(it -> {
it.fireEvent(new DialogCloseEvent());
onEnd();
@@ -51,10 +51,10 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
pane.setTitle(i18n("message.doing"));
if (settings.containsKey("title")) {
Object title = settings.get("title");
if (title instanceof StringProperty)
pane.titleProperty().bind((StringProperty) title);
else if (title instanceof String)
pane.setTitle((String) title);
if (title instanceof StringProperty titleProperty)
pane.titleProperty().bind(titleProperty);
else if (title instanceof String titleMessage)
pane.setTitle(titleMessage);
}
runInFX(() -> {
@@ -63,8 +63,8 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
public void onStop(boolean success, TaskExecutor executor) {
runInFX(() -> {
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
Controllers.dialog((String) settings.get("success_message"), null, MessageType.SUCCESS, () -> onEnd());
if (settings.get("success_message") instanceof String successMessage)
Controllers.dialog(successMessage, null, MessageType.SUCCESS, () -> onEnd());
else if (!settings.containsKey("forbid_success_message"))
Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd());
} else {
@@ -77,10 +77,10 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
}
String appendix = StringUtils.getStackTrace(executor.getException());
if (settings.get("failure_callback") instanceof WizardProvider.FailureCallback)
((WizardProvider.FailureCallback) settings.get("failure_callback")).onFail(settings, executor.getException(), () -> onEnd());
else if (settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageType.ERROR, () -> onEnd());
if (settings.get(WizardProvider.FailureCallback.KEY) != null)
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd());
else if (settings.get("failure_message") instanceof String failureMessage)
Controllers.dialog(appendix, failureMessage, MessageType.ERROR, () -> onEnd());
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, i18n("wizard.failed"), MessageType.ERROR, () -> onEnd());
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.SettingsMap;
import java.util.*;
@@ -27,7 +28,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class WizardController implements Navigation {
private final WizardDisplayer displayer;
private WizardProvider provider = null;
private final Map<String, Object> settings = new HashMap<>();
private final SettingsMap settings = new SettingsMap();
private final Stack<Node> pages = new Stack<>();
private boolean stopped = false;
@@ -36,7 +37,7 @@ public class WizardController implements Navigation {
}
@Override
public Map<String, Object> getSettings() {
public SettingsMap getSettings() {
return settings;
}
@@ -19,8 +19,7 @@ package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
public interface WizardDisplayer {
default void onStart() {
@@ -34,5 +33,5 @@ public interface WizardDisplayer {
void navigateTo(Node page, Navigation.NavigationDirection nav);
void handleTask(Map<String, Object> settings, Task<?> task);
void handleTask(SettingsMap settings, Task<?> task);
}
@@ -17,13 +17,13 @@
*/
package org.jackhuang.hmcl.ui.wizard;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
public interface WizardPage {
default void onNavigate(Map<String, Object> settings) {
default void onNavigate(SettingsMap settings) {
}
default void cleanup(Map<String, Object> settings) {
default void cleanup(SettingsMap settings) {
}
String getTitle();
@@ -18,15 +18,14 @@
package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
public interface WizardProvider {
void start(Map<String, Object> settings);
void start(SettingsMap settings);
Object finish(Map<String, Object> settings);
Object finish(SettingsMap settings);
Node createPage(WizardController controller, int step, Map<String, Object> settings);
Node createPage(WizardController controller, int step, SettingsMap settings);
boolean cancel();
@@ -35,6 +34,8 @@ public interface WizardProvider {
}
interface FailureCallback {
void onFail(Map<String, Object> settings, Exception exception, Runnable next);
SettingsMap.Key<FailureCallback> KEY = new SettingsMap.Key<>("failure_callback");
void onFail(SettingsMap settings, Exception exception, Runnable next);
}
}
@@ -18,8 +18,7 @@
package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.control.Control;
import java.util.Map;
import org.jackhuang.hmcl.util.SettingsMap;
public abstract class WizardSinglePage extends Control implements WizardPage {
protected final Runnable onFinish;
@@ -31,6 +30,6 @@ public abstract class WizardSinglePage extends Control implements WizardPage {
protected abstract Object finish();
@Override
public void cleanup(Map<String, Object> settings) {
public void cleanup(SettingsMap settings) {
}
}
@@ -24,9 +24,9 @@ import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.windows.WindowsVersion;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -181,13 +181,13 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull WindowsVersion windowsVersion) {
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.compareTo(WindowsVersion.WINDOWS_10) >= 0) {
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
} else if (windowsVersion.compareTo(WindowsVersion.WINDOWS_7) >= 0) {
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
else
@@ -92,10 +92,10 @@ public final class SelfDependencyPatcher {
} else {
defaultRepository = Repository.MAVEN_CENTRAL;
}
repositories = Collections.unmodifiableList(Arrays.asList(Repository.MAVEN_CENTRAL, Repository.TENCENTCLOUD_MIRROR));
repositories = List.of(Repository.MAVEN_CENTRAL, Repository.TENCENTCLOUD_MIRROR);
} else {
defaultRepository = new Repository(String.format(i18n("repositories.custom"), customUrl), customUrl);
repositories = Collections.unmodifiableList(Arrays.asList(Repository.MAVEN_CENTRAL, Repository.TENCENTCLOUD_MIRROR, defaultRepository));
repositories = List.of(Repository.MAVEN_CENTRAL, Repository.TENCENTCLOUD_MIRROR, defaultRepository);
}
}
@@ -112,7 +112,7 @@ public final class SelfDependencyPatcher {
if (platform == null)
return null;
if (JavaRuntime.CURRENT_VERSION >= 22) {
if (JavaRuntime.CURRENT_VERSION >= 23) {
List<DependencyDescriptor> modernDependencies = platform.get("modern");
if (modernDependencies != null)
return modernDependencies;
@@ -143,18 +143,10 @@ public final class SelfDependencyPatcher {
}
}
private static final class Repository {
private record Repository(String name, String url) {
public static final Repository MAVEN_CENTRAL = new Repository(i18n("repositories.maven_central"), "https://repo1.maven.org/maven2");
public static final Repository TENCENTCLOUD_MIRROR = new Repository(i18n("repositories.tencentcloud_mirror"), "https://mirrors.cloud.tencent.com/nexus/repository/maven-public");
private final String name;
private final String url;
Repository(String name, String url) {
this.name = name;
this.url = url;
}
public String resolveDependencyURL(DependencyDescriptor descriptor) {
return String.format("%s/%s/%s/%s/%s",
url,
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -34,7 +33,7 @@ public final class I18n {
private I18n() {
}
private static volatile SupportedLocale locale = Locales.DEFAULT;
private static volatile SupportedLocale locale = SupportedLocale.DEFAULT;
public static void setLocale(SupportedLocale locale) {
I18n.locale = locale;
@@ -44,6 +43,10 @@ public final class I18n {
return locale;
}
public static boolean isUpsideDown() {
return LocaleUtils.getScript(locale.getLocale()).equals("Qabs");
}
public static boolean isUseChinese() {
return LocaleUtils.isChinese(locale.getLocale());
}
@@ -71,6 +74,11 @@ public final class I18n {
else
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
}
if (LocaleUtils.isEnglish(locale.getLocale()) && "Qabs".equals(LocaleUtils.getScript(locale.getLocale()))) {
return UpsideDownUtils.translate(version.getSelfVersion());
}
return version.getSelfVersion();
}
@@ -1,273 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class Locales {
private Locales() {
}
public static final SupportedLocale DEFAULT;
static {
String language = System.getenv("HMCL_LANGUAGE");
DEFAULT = new SupportedLocale(true, "def",
StringUtils.isBlank(language) ? LocaleUtils.SYSTEM_DEFAULT : Locale.forLanguageTag(language));
}
/**
* English
*/
public static final SupportedLocale EN = new SupportedLocale("en");
/**
* Spanish
*/
public static final SupportedLocale ES = new SupportedLocale("es");
/**
* Russian
*/
public static final SupportedLocale RU = new SupportedLocale("ru");
/**
* Ukrainian
*/
public static final SupportedLocale UK = new SupportedLocale("uk");
/**
* Japanese
*/
public static final SupportedLocale JA = new SupportedLocale("ja");
/**
* Chinese (Simplified)
*/
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", LocaleUtils.LOCALE_ZH_HANS);
/**
* Chinese (Traditional)
*/
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", LocaleUtils.LOCALE_ZH_HANT);
/**
* Wenyan (Classical Chinese)
*/
public static final SupportedLocale WENYAN = new SupportedLocale("lzh");
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
for (SupportedLocale locale : LOCALES) {
if (locale.getName().equalsIgnoreCase(name))
return locale;
}
return DEFAULT;
}
@JsonAdapter(SupportedLocale.TypeAdapter.class)
public static final class SupportedLocale {
private final boolean isDefault;
private final String name;
private final Locale locale;
private ResourceBundle resourceBundle;
private DateTimeFormatter dateTimeFormatter;
private List<Locale> candidateLocales;
SupportedLocale(boolean isDefault, String name, Locale locale) {
this.isDefault = isDefault;
this.name = name;
this.locale = locale;
}
SupportedLocale(String name) {
this(false, name, Locale.forLanguageTag(name));
}
SupportedLocale(String name, Locale locale) {
this(false, name, locale);
}
public boolean isDefault() {
return isDefault;
}
public String getName() {
return name;
}
public Locale getLocale() {
return locale;
}
public String getDisplayName(SupportedLocale inLocale) {
if (isDefault()) {
try {
return inLocale.getResourceBundle().getString("lang.default");
} catch (Throwable e) {
LOG.warning("Failed to get localized name for default locale", e);
return "Default";
}
}
Locale inJavaLocale = inLocale.getLocale();
if (LocaleUtils.isISO3Language(inJavaLocale.getLanguage())) {
String iso1 = LocaleUtils.getISO1Language(inJavaLocale);
if (LocaleUtils.isISO1Language(iso1)) {
Locale.Builder builder = new Locale.Builder()
.setLocale(inJavaLocale)
.setLanguage(iso1);
if (inJavaLocale.getScript().isEmpty())
builder.setScript(LocaleUtils.getScript(inJavaLocale));
inJavaLocale = builder.build();
}
}
if (this.locale.getLanguage().equals("lzh")) {
if (inJavaLocale.getLanguage().equals("zh"))
return "文言";
String name = locale.getDisplayName(inJavaLocale);
return name.equals("lzh") || name.equals("Literary Chinese")
? "Chinese (Classical)"
: name;
}
return locale.getDisplayName(inJavaLocale);
}
public ResourceBundle getResourceBundle() {
ResourceBundle bundle = resourceBundle;
if (resourceBundle == null)
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale,
DefaultResourceBundleControl.INSTANCE);
return bundle;
}
public List<Locale> getCandidateLocales() {
if (candidateLocales == null)
candidateLocales = List.copyOf(LocaleUtils.getCandidateLocales(locale));
return candidateLocales;
}
public String i18n(String key, Object... formatArgs) {
try {
return String.format(getResourceBundle().getString(key), formatArgs);
} catch (MissingResourceException e) {
LOG.error("Cannot find key " + key + " in resource bundle", e);
} catch (IllegalFormatException e) {
LOG.error("Illegal format string, key=" + key + ", args=" + Arrays.toString(formatArgs), e);
}
return key + Arrays.toString(formatArgs);
}
public String i18n(String key) {
try {
return getResourceBundle().getString(key);
} catch (MissingResourceException e) {
LOG.error("Cannot find key " + key + " in resource bundle", e);
return key;
}
}
public String formatDateTime(TemporalAccessor time) {
DateTimeFormatter formatter = dateTimeFormatter;
if (formatter == null) {
if (locale.getLanguage().equals("lzh"))
return WenyanUtils.formatDateTime(time);
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
.withZone(ZoneId.systemDefault());
}
return formatter.format(time);
}
public String getFcMatchPattern() {
String language = locale.getLanguage();
String region = locale.getCountry();
if (LocaleUtils.isEnglish(locale))
return "";
if (LocaleUtils.isChinese(locale)) {
String lang;
String charset;
String script = LocaleUtils.getScript(locale);
switch (script) {
case "Hans":
lang = region.equals("SG") || region.equals("MY")
? "zh-" + region
: "zh-CN";
charset = "0x6e38,0x620f";
break;
case "Hant":
lang = region.equals("HK") || region.equals("MO")
? "zh-" + region
: "zh-TW";
charset = "0x904a,0x6232";
break;
default:
return "";
}
return ":lang=" + lang + ":charset=" + charset;
}
return region.isEmpty() ? language : language + "-" + region;
}
public boolean isSameLanguage(SupportedLocale other) {
return LocaleUtils.getISO1Language(this.getLocale())
.equals(LocaleUtils.getISO1Language(other.getLocale()));
}
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
@Override
public void write(JsonWriter out, SupportedLocale value) throws IOException {
out.value(value.getName());
}
@Override
public SupportedLocale read(JsonReader in) throws IOException {
return getLocaleByName(in.nextString());
}
}
}
}
@@ -27,7 +27,7 @@ public final class MinecraftWiki {
private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("^[0-9]{2}w[0-9]{2}.+$");
public static String getWikiLink(Locales.SupportedLocale locale, GameRemoteVersion version) {
public static String getWikiLink(SupportedLocale locale, GameRemoteVersion version) {
String wikiVersion = version.getSelfVersion();
var gameVersion = GameVersionNumber.asGameVersion(wikiVersion);
@@ -41,7 +41,7 @@ public final class MinecraftWiki {
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) {
return getWikiLink(Locales.ZH_HANT, version);
return getWikiLink(SupportedLocale.getLocale(LocaleUtils.LOCALE_ZH_HANT), version);
} else if (SNAPSHOT_PATTERN.matcher(wikiVersion).matches()) {
return locale.i18n("wiki.version.game.snapshot", translatedVersion);
} else {
@@ -0,0 +1,280 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonAdapter(SupportedLocale.TypeAdapter.class)
public final class SupportedLocale {
public static final SupportedLocale DEFAULT = new SupportedLocale();
private static final ConcurrentMap<Locale, SupportedLocale> LOCALES = new ConcurrentHashMap<>();
public static List<SupportedLocale> getSupportedLocales() {
List<SupportedLocale> list = new ArrayList<>();
list.add(DEFAULT);
InputStream locales = SupportedLocale.class.getResourceAsStream("/assets/lang/languages.json");
if (locales != null) {
try (locales) {
list.addAll(JsonUtils.fromNonNullJsonFully(locales, JsonUtils.listTypeOf(SupportedLocale.class)));
} catch (Throwable e) {
LOG.warning("Failed to load languages.json", e);
}
}
return List.copyOf(list);
}
public static SupportedLocale getLocale(Locale locale) {
return LOCALES.computeIfAbsent(locale, SupportedLocale::new);
}
public static SupportedLocale getLocaleByName(String name) {
if (name == null || name.isBlank() || "def".equals(name) || "default".equals(name))
return DEFAULT;
return getLocale(Locale.forLanguageTag(name.trim().replace('_', '-')));
}
private final boolean isDefault;
private final String name;
private final Locale locale;
private ResourceBundle resourceBundle;
private ResourceBundle localeNamesBundle;
private DateTimeFormatter dateTimeFormatter;
private List<Locale> candidateLocales;
SupportedLocale() {
this.isDefault = true;
this.name = "def"; // TODO: Change to "default" after updating the Config format
String language = System.getenv("HMCL_LANGUAGE");
this.locale = StringUtils.isBlank(language)
? LocaleUtils.SYSTEM_DEFAULT
: Locale.forLanguageTag(language);
}
SupportedLocale(Locale locale) {
this.isDefault = false;
this.name = locale.toLanguageTag();
this.locale = locale;
}
public boolean isDefault() {
return isDefault;
}
public String getName() {
return name;
}
public Locale getLocale() {
return locale;
}
public String getDisplayName(SupportedLocale inLocale) {
if (isDefault()) {
try {
return inLocale.getResourceBundle().getString("lang.default");
} catch (Throwable e) {
LOG.warning("Failed to get localized name for default locale", e);
return "Default";
}
}
Locale currentLocale = this.getLocale();
String language = currentLocale.getLanguage();
String subLanguage;
{
String parentLanguage = LocaleUtils.getParentLanguage(language);
if (parentLanguage != null) {
subLanguage = language;
language = parentLanguage;
} else {
subLanguage = "";
}
}
ResourceBundle localeNames = inLocale.getLocaleNamesBundle();
String languageDisplayName = language;
try {
languageDisplayName = localeNames.getString(language);
} catch (Throwable e) {
LOG.warning("Failed to get localized name for language " + language, e);
}
// Currently, HMCL does not support any locales with regions or variants, so they are not handled for now
List<String> subTags = Stream.of(subLanguage, currentLocale.getScript())
.filter(it -> !it.isEmpty())
.map(it -> {
try {
return localeNames.getString(it);
} catch (Throwable e) {
LOG.warning("Failed to get localized name of " + it, e);
}
return it;
}).toList();
return subTags.isEmpty()
? languageDisplayName
: languageDisplayName + " (" + String.join(", ", subTags) + ")";
}
public ResourceBundle getResourceBundle() {
ResourceBundle bundle = resourceBundle;
if (resourceBundle == null)
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale,
DefaultResourceBundleControl.INSTANCE);
return bundle;
}
public ResourceBundle getLocaleNamesBundle() {
ResourceBundle bundle = localeNamesBundle;
if (localeNamesBundle == null)
localeNamesBundle = bundle = ResourceBundle.getBundle("assets.lang.LocaleNames", locale,
DefaultResourceBundleControl.INSTANCE);
return bundle;
}
public List<Locale> getCandidateLocales() {
if (candidateLocales == null)
candidateLocales = List.copyOf(LocaleUtils.getCandidateLocales(locale));
return candidateLocales;
}
public String i18n(String key, Object... formatArgs) {
try {
return String.format(getResourceBundle().getString(key), formatArgs);
} catch (MissingResourceException e) {
LOG.error("Cannot find key " + key + " in resource bundle", e);
} catch (IllegalFormatException e) {
LOG.error("Illegal format string, key=" + key + ", args=" + Arrays.toString(formatArgs), e);
}
return key + Arrays.toString(formatArgs);
}
public String i18n(String key) {
try {
return getResourceBundle().getString(key);
} catch (MissingResourceException e) {
LOG.error("Cannot find key " + key + " in resource bundle", e);
return key;
}
}
public String formatDateTime(TemporalAccessor time) {
DateTimeFormatter formatter = dateTimeFormatter;
if (formatter == null) {
if (LocaleUtils.isEnglish(locale) && "Qabs".equals(locale.getScript())) {
return UpsideDownUtils.formatDateTime(time);
}
if (locale.getLanguage().equals("lzh")) {
return WenyanUtils.formatDateTime(time);
}
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
.withZone(ZoneId.systemDefault());
}
return formatter.format(time);
}
public String getFcMatchPattern() {
String language = locale.getLanguage();
String region = locale.getCountry();
if (LocaleUtils.isEnglish(locale))
return "";
if (LocaleUtils.isChinese(locale)) {
String lang;
String charset;
String script = LocaleUtils.getScript(locale);
switch (script) {
case "Hans":
lang = region.equals("SG") || region.equals("MY")
? "zh-" + region
: "zh-CN";
charset = "0x6e38,0x620f";
break;
case "Hant":
lang = region.equals("HK") || region.equals("MO")
? "zh-" + region
: "zh-TW";
charset = "0x904a,0x6232";
break;
default:
return "";
}
return ":lang=" + lang + ":charset=" + charset;
}
return region.isEmpty() ? language : language + "-" + region;
}
public boolean isSameLanguage(SupportedLocale other) {
return LocaleUtils.getRootLanguage(this.getLocale())
.equals(LocaleUtils.getRootLanguage(other.getLocale()));
}
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
@Override
public void write(JsonWriter out, SupportedLocale value) throws IOException {
out.value(value.getName());
}
@Override
public SupportedLocale read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL)
return DEFAULT;
String language = in.nextString();
return getLocaleByName(switch (language) {
// TODO: Remove these compatibility codes after updating the Config format
case "zh_CN" -> "zh-Hans"; // For compatibility
case "zh" -> "zh-Hant"; // For compatibility
default -> language;
});
}
}
}
@@ -0,0 +1,77 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class UpsideDownUtils {
private static final Map<Integer, Integer> MAPPER = loadMap();
private static Map<Integer, Integer> loadMap() {
var map = new LinkedHashMap<Integer, Integer>();
InputStream inputStream = UpsideDownUtils.class.getResourceAsStream("/assets/lang/upside_down.txt");
if (inputStream != null) {
try (inputStream) {
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
if (line.isBlank() || line.startsWith("#"))
return;
if (line.length() != 2) {
LOG.warning("Invalid line: " + line);
return;
}
map.put((int) line.charAt(0), (int) line.charAt(1));
});
} catch (IOException e) {
LOG.warning("Failed to load upside_down.txt", e);
}
} else {
LOG.warning("upside_down.txt not found");
}
return Collections.unmodifiableMap(map);
}
public static String translate(String str) {
StringBuilder builder = new StringBuilder(str.length());
str.codePoints().forEach(ch -> builder.appendCodePoint(MAPPER.getOrDefault(ch, ch)));
return builder.reverse().toString();
}
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
.withZone(ZoneId.systemDefault());
public static String formatDateTime(TemporalAccessor time) {
return translate(BASE_FORMATTER.format(time));
}
private UpsideDownUtils() {
}
}
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff. Show more