Compare commits

...
72 Commits
Author SHA1 Message Date
Glavo 86085031f6 优化自动切换游戏语言功能 (#4353) 2025-08-31 14:57:23 +08:00
Glavo 30f47704f1 更新文言文翻译 (#4364) 2025-08-31 12:18:34 +08:00
Glavo 82e0cd1ca1 Fix #3669: 修复首次打开子页面时切换动画丢失的问题 (#4365) 2025-08-31 12:11:13 +08:00
Wulian233andGlavo 5374127241 修改部分文言条目 (#4359)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-31 00:59:14 +08:00
Glavo df489c0cfe 添加乌克兰语支持 (#4363) 2025-08-31 00:48:22 +08:00
BANERandGlavo d83f6106f5 Add I18N_uk.properties (#4346)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-31 00:33:16 +08:00
Glavo b7361c8da4 优化语言设置 (#4362) 2025-08-31 00:18:25 +08:00
Glavo 73531dbf60 优化文言文翻译 (#4361) 2025-08-30 22:21:45 +08:00
Andypsl8 5af6858d4c 更新文言文翻译 (#4356) 2025-08-30 02:06:33 +08:00
Glavo 1a3b473356 Fix #3076: 修复无法识别部分简体中文环境的问题 (#4350) 2025-08-29 20:31:05 +08:00
Glavo 67e4dddda1 添加文言文支持 (#4348) 2025-08-29 14:42:10 +08:00
Glavo 55816032b7 启动游戏时隐藏 GameAssetDownloadTask (#4349) 2025-08-29 12:25:49 +08:00
Glavo b9fd0e50a7 为迁移至 Java 17 做准备 (#4311) 2025-08-28 20:43:03 +08:00
Glavo 71d54955ad 修复 TaskListPane.StatusChangeListener 持有 ListCell 的强引用的问题 (#4347) 2025-08-28 20:21:55 +08:00
Burning_TNTandGlavo d34dc82db6 Feature: Decompress legacy HMCL log files while exporting. (#4342)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-27 21:44:33 +08:00
Glavo 73214ad599 不对回环地址应用代理 (#4338) 2025-08-27 21:43:54 +08:00
Wulian233 1ddb73595d 修复世界管理页面不支持显示极限模式的问题 (#4321) 2025-08-27 21:41:52 +08:00
Glavo f9ee9c8592 更严格地检测文件名合法性 (#4341) 2025-08-27 20:10:52 +08:00
Burning_TNT 0fdfc945c3 Fix: IJ support (#4343) 2025-08-26 22:43:26 +08:00
YuChuXi 97219c951e 游戏崩溃窗口中增加对拍照的提醒 (#4294) 2025-08-26 21:54:42 +08:00
Glavo 64cc64095d 增加 TaskListPane 中任务条目之间的间距 (#4340) 2025-08-26 21:51:59 +08:00
辞庐 91a613d966 HintPane 使用新版 Bootstrap 调色盘 (#4323) 2025-08-24 22:30:14 +08:00
Glavo 06b7b1e574 Fix #4325: 修复未将 15w14a 识别为愚人节版本的问题 (#4327) 2025-08-24 22:27:49 +08:00
Glavo 933b7cab2f 将 URI 中的错误代理对编码为 U+FFFD (#4329) 2025-08-24 22:27:18 +08:00
Glavo 885e25d07a 更新文档 (#4328) 2025-08-24 20:33:13 +08:00
Glavo 421f9a3b2b 支持以字符串形式序列化 VersionSetting 中的枚举成员 (#4299) 2025-08-21 21:56:49 +08:00
Glavo e52cfb036e 优化 Task.getCaller() (#4308) 2025-08-21 20:53:19 +08:00
3gf8jv4dv 87a3a4ece3 chore(locale): add new translations for russian and spanish (#4304) 2025-08-21 20:45:40 +08:00
3gf8jv4dv b152e2bfab fix(locale): add missing i18n when installing an instance with quilt api (#4303) 2025-08-21 20:45:08 +08:00
Glavo c9c97a45b7 修复 TaskListPane 中任务总数显示错误的问题 (#4310) 2025-08-21 20:43:19 +08:00
Glavo cba4ba2799 删除无用的 debug 日志 (#4309) 2025-08-21 20:42:51 +08:00
3gf8jv4dv ab9eea3620 fix(locale): revise mcmod-info-mirror description on thanks list (#4305) 2025-08-21 20:42:22 +08:00
Glavoand3gf8jv4dv 96f2d011ca 优化 TaskListPane 的显示效果 (#4306)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-21 16:45:07 +08:00
Glavo 9ee55cd17d 修复 ModrinthModpackExportTask 名称未被翻译的问题 (#4301) 2025-08-20 21:47:56 +08:00
Glavo 07ee7867da 修复 VersionIconType 成员的排列顺序 (#4298) 2025-08-20 21:20:31 +08:00
Glavo 940d4f722c 修复 TaskListPane 可能出现横向滚动条的问题 (#4297) 2025-08-20 20:34:12 +08:00
Glavo af56096162 优化对于 IntelliJ IDEA 的支持 (#4270) 2025-08-20 20:33:03 +08:00
Glavo 0e12c1a132 Fix #4290: 在 macOS 上禁止最大化和全屏模式 (#4293) 2025-08-20 15:40:48 +08:00
Glavo ca20509cb1 优化 TaskListPane (#4292) 2025-08-20 00:32:40 +08:00
HenryxjhandGlavo 58c93ff349 修复 Windows 平台 GPU 信息可能显示为 "[B@xxx" 的问题 (#4288)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-19 21:33:10 +08:00
Glavo aab3b22290 修复 TaskListPane 未正确处理 StagesHintTask 的问题 (#4285) 2025-08-19 16:22:51 +08:00
Wulian233 6cf933741f 基于实际屏幕分辨率添加游戏窗口分辨率选项 (#4275) 2025-08-18 20:05:34 +08:00
Glavo 9d17834227 Fix #4281: 支持解析 Indev 版本号 (#4282) 2025-08-18 20:04:22 +08:00
Glavo 7805508d4c Fix #2676: 优化版本号排序规则 (#4276) 2025-08-18 16:24:03 +08:00
Wulian233 72f6c2faf3 修复 1.21.5 以上的地图跳转种子地图错误 (#4274) 2025-08-18 16:21:07 +08:00
Glavo 7035767828 修复 pending 类型的游戏版本被错误识别为愚人节版的问题 (#4273) 2025-08-16 16:08:58 +08:00
Wulian233andGlavo 13a2777c87 为 NeoForge 下载添加正式版/快照分类 (#4268)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-16 15:58:31 +08:00
Glavo a4f9438d9e 移除 Pack200 支持 (#4267) 2025-08-16 15:45:12 +08:00
Glavo 4445021d66 Fix #4061: 在 Windows 7/8/8.1 上使用 llvmpipe-loader 加载 mesa (#4224) 2025-08-14 23:07:23 +08:00
Glavo f6a3b76a0b 暂时禁用 MCIM 加速 (#4265) 2025-08-14 20:20:46 +08:00
Glavo 42bb602ff5 优化游戏下载页 (#4264) 2025-08-14 20:14:40 +08:00
Glavo 4547448718 优化游戏窗口大小选择功能 (#4234) 2025-08-14 16:32:14 +08:00
Glavo 10bda7e6f4 转义 URI 中的中括号和大括号 (#4261) 2025-08-14 16:16:43 +08:00
Glavo f239140055 Update amdgpu.ids (#4262) 2025-08-14 15:54:30 +08:00
Glavo 6f4b557f63 支持最大化和全屏模式 (#4260) 2025-08-14 15:10:02 +08:00
Glavo 5118ce4e62 Fix #4256: 修复游戏下载页面可能崩溃的问题 (#4257) 2025-08-13 16:14:26 +08:00
ZkiteflyandGlavo a705b7cf95 Fix 3955: 启动时不应补全服务端自动更新整合包中已禁用的 mod (#3956)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-13 16:03:53 +08:00
GlavoandCopilot 0d5d1ae4e6 将 authlib-injector 嵌入启动器 (#4246)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 15:37:09 +08:00
Glavo b57547415b 在未安装游戏版本时支持一键下载启动游戏 (#4248) 2025-08-13 15:36:53 +08:00
Wulian233andGlavo b60d889bf7 为愚人节分类添加图标 (#4249)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-13 15:17:47 +08:00
Glavo 2f463c1eda 在 ./.hmcl/java 中搜索 Java (#4179) 2025-08-11 20:59:30 +08:00
Wulian233 d1e006ed29 修复版本列表排序错误的问题 (#4244) 2025-08-11 20:57:05 +08:00
GlavoandCopilot ba58905c9c 优化游戏下载页版本分类功能 (#4141)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-11 16:52:03 +08:00
Glavo de221b9dae 修复非 Windows 平台上游戏进程的 user.home 可能被设置为 null 的问题 (#4241) 2025-08-11 16:08:27 +08:00
Glavo 0913c5e710 支持通过 MCIM 加速模组搜索 (#4225) 2025-08-11 16:06:59 +08:00
ZkiteflyandGlavo 41fd38d8d1 导出日志时附带最近日志 (#4051)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-10 21:55:01 +08:00
ENC_EuphonyandGlavo 7ec880852f 修复部分版本 wiki 链接不正确的问题 (#4239)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-10 21:41:44 +08:00
Glavo 9213b7fb2e Fix 4232: 修复初始化 CacheRepository 导致启动器崩溃的问题 (#4235) 2025-08-10 21:41:10 +08:00
Glavo 80f1367d9a Fix #4233: 修复 RemoteMod::getIntegrityCheck 未选择正确的算法的问题 (#4238) 2025-08-10 15:39:13 +08:00
Glavo f521a041a9 Bump Shadow Plugin to 9.0.1 (#4220) 2025-08-10 15:23:44 +08:00
be9efbe6ee 修复版本 Wiki 映射 (#4211)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-10 15:06:31 +08:00
辞庐and3gf8jv4dv 8f3c716ca7 Fix #4127: 修改整合包下载页面描述 (#4200)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-10 15:05:57 +08:00
98 changed files with 5245 additions and 1115 deletions

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ hs_err_pid*
/externalgames
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
# gradle build
/build/
+77 -33
View File
@@ -37,6 +37,8 @@ val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
version = "$versionRoot.$buildNumber"
val embedResources by configurations.registering
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
@@ -47,6 +49,8 @@ dependencies {
if (launcherExe == null) {
implementation(libs.hmclauncher)
}
embedResources(libs.authlib.injector)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -104,6 +108,50 @@ tasks.compileJava {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
}
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile by tasks.registering {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
doLast {
val targetFile = hmclPropertiesFile.get().asFile
targetFile.parentFile.mkdir()
targetFile.bufferedWriter().use {
for ((k, v) in hmclProperties) {
it.write("$k=$v\n")
}
}
}
}
val addOpens = listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)
tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
@@ -112,6 +160,8 @@ tasks.jar {
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.shadowJar {
dependsOn(createPropertiesFile)
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
@@ -132,39 +182,14 @@ tasks.shadowJar {
exclude(project(":HMCLBoot"))
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
).joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED"
)
System.getenv("GITHUB_SHA")?.also {
attributes("GitHub-SHA" to it)
}
}
manifest.attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Implementation-Version" to project.version.toString(),
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Add-Opens" to addOpens.joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED"
)
if (launcherExe != null) {
into("assets") {
@@ -178,6 +203,15 @@ tasks.shadowJar {
}
}
tasks.processResources {
dependsOn(createPropertiesFile)
into("assets/") {
from(hmclPropertiesFile)
from(embedResources)
}
}
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
@@ -260,6 +294,16 @@ fun parseToolOptions(options: String?): MutableList<String> {
return result
}
// For IntelliJ IDEA
tasks.withType<JavaExec> {
if (name != "run") {
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
// if (javaVersion >= JavaVersion.VERSION_24) {
// jvmArgs("--enable-native-access=ALL-UNNAMED")
// }
}
}
tasks.register<JavaExec>("run") {
dependsOn(tasks.jar)
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -19,10 +19,13 @@ package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
* Stores metadata about this application.
@@ -33,11 +36,15 @@ public final class Metadata {
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 11;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final int RECOMMENDED_JAVA_VERSION = 21;
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
@@ -49,8 +56,8 @@ public final class Metadata {
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
@@ -94,4 +101,30 @@ public final class Metadata {
return !isStable() && !isDev();
}
public static @Nullable String getSuggestedJavaDownloadLink() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
else
return null;
}
}
}
@@ -21,15 +21,18 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales;
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.util.Map;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
@@ -58,61 +61,83 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists()) {
if (config().isDisableAutoGameOptions())
return;
}
if (configFolder.isDirectory()) {
if (findFiles(configFolder, "options.txt")) {
return;
Path runDir = repository.getRunDirectory(version.getId()).toPath();
Path optionsFile = runDir.resolve("options.txt");
Path configFolder = runDir.resolve("config");
if (Files.exists(optionsFile))
return;
if (Files.isDirectory(configFolder)) {
try (Stream<Path> stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) {
if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file))))
return;
} catch (IOException e) {
LOG.warning("Failed to visit config folder", e);
}
}
if (!I18n.isUseChinese()) {
Locale locale = Locale.getDefault();
if (Locales.isEnglish(locale))
return;
}
String lang;
/*
1.0- :没有语言选项,遇到这些版本时不设置
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
* 1.0 : No language option, do not set for these versions
* 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash)
* 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
*/
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
if (gameVersion.compareTo("1.1") < 0) {
lang = null;
} else if (gameVersion.compareTo("1.11") < 0) {
lang = "zh_CN";
} else {
lang = "zh_cn";
}
if (gameVersion.compareTo("1.1") < 0)
return;
if (lang != null) {
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
String lang = normalizedLanguageTag(locale, gameVersion);
if (lang.isEmpty())
return;
if (gameVersion.compareTo("1.11") >= 0)
lang = lang.toLowerCase(Locale.ROOT);
try {
Files.createDirectories(optionsFile.getParent());
Files.writeString(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
}
private boolean findFiles(File folder, String fileName) {
File[] fs = folder.listFiles();
if (fs != null) {
for (File f : fs) {
if (f.isDirectory())
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
return true;
if (f.getName().equals(fileName))
return true;
}
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
String language = locale.getLanguage();
String region = locale.getCountry();
switch (language) {
case "zh":
case "cmn":
if (Locales.isSimplifiedChinese(locale))
return "zh_CN";
if (gameVersion.compareTo("1.16") >= 0
&& (region.equals("HK") || region.equals("MO")))
return "zh_HK";
return "zh_TW";
case "ru":
return "ru_RU";
case "uk":
return "uk_UA";
case "es":
return "es_ES";
case "ja":
return "ja_JP";
case "lzh":
return gameVersion.compareTo("1.16") >= 0
? "lzh"
: "";
default:
return "";
}
return false;
}
@Override
@@ -160,8 +160,10 @@ public final class LauncherHelper {
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(java);
if (renderer != Renderer.DEFAULT
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& OperatingSystem.WINDOWS_VERSION != null) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
{
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
if (chineseIndex > englishIndex) {
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
@@ -50,10 +50,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -210,14 +207,17 @@ public final class ModpackHelper {
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
@@ -158,13 +158,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
}
@Override
@@ -56,6 +56,7 @@ public final class JavaManager {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
@@ -571,6 +572,10 @@ public final class JavaManager {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import javax.net.ssl.SSLException;
@@ -68,18 +69,6 @@ public final class Accounts {
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
private static void triggerAuthlibInjectorUpdateCheck() {
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
Schedulers.io().execute(() -> {
try {
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
} catch (IOException e) {
LOG.warning("Failed to check update for authlib-injector", e);
}
});
}
}
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
@@ -337,8 +326,6 @@ public final class Accounts {
});
}
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@@ -371,24 +358,18 @@ public final class Accounts {
// ==== authlib-injector ====
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
if (local.isPresent()) {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
}
};
} else {
if (authlibinjectorLocation != null) {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
String authlibInjectorVersion = JarUtils.getAttribute("hmcl.authlib-injector.version", null);
if (authlibInjectorVersion == null)
throw new AssertionError("Missing hmcl.authlib-injector.version");
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
@@ -186,6 +186,9 @@ public final class Config implements Observable {
@SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@SerializedName("disableAutoGameOptions")
private BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false);
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@@ -631,6 +634,18 @@ public final class Config implements Observable {
this.addedLittleSkin.set(addedLittleSkin);
}
public BooleanProperty disableAutoGameOptionsProperty() {
return disableAutoGameOptions;
}
public boolean isDisableAutoGameOptions() {
return disableAutoGameOptions.get();
}
public void setDisableAutoGameOptions(boolean disableAutoGameOptions) {
this.disableAutoGameOptions.set(disableAutoGameOptions);
}
public int getConfigVersion() {
return configVersion.get();
}
@@ -129,8 +129,12 @@ public final class FontManager {
return null;
try {
Locale locale = Locale.getDefault();
if (locale.getLanguage().equals("lzh"))
locale = Locale.TRADITIONAL_CHINESE;
String result = SystemUtils.run(fcMatch.toString(),
":lang=" + Locale.getDefault().toLanguageTag(),
":lang=" + locale.toLanguageTag(),
"--format", "%{family}\\n%{file}").trim();
String[] results = result.split("\\n");
@@ -19,22 +19,28 @@ package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.*;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static final SimpleProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT;
static {
ProxySelector systemProxySelector = ProxySelector.getDefault();
SYSTEM_DEFAULT = systemProxySelector != null
? new ProxySelectorWrapper(systemProxySelector)
: NO_PROXY;
}
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
@@ -51,7 +57,7 @@ public final class ProxyManager {
LOG.warning("Illegal proxy port: " + port);
return NO_PROXY;
} else {
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
return new ProxySelectorWrapper(new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port))));
}
} else {
return ProxyManager.SYSTEM_DEFAULT;
@@ -106,15 +112,41 @@ public final class ProxyManager {
config().proxyPassProperty().addListener(updateAuthenticator);
}
private static final class SimpleProxySelector extends ProxySelector {
private static abstract class AbstractProxySelector extends ProxySelector {
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (uri == null || sa == null || ioe == null) {
throw new IllegalArgumentException("Arguments can't be null.");
}
}
}
private static final class SimpleProxySelector extends AbstractProxySelector {
private final List<Proxy> proxies;
SimpleProxySelector(Proxy proxy) {
this(Collections.singletonList(proxy));
this.proxies = List.of(proxy);
}
SimpleProxySelector(List<Proxy> proxies) {
this.proxies = proxies;
@Override
public List<Proxy> select(URI uri) {
if (uri == null)
throw new IllegalArgumentException("URI can't be null.");
return proxies;
}
@Override
public String toString() {
return "SimpleProxySelector" + proxies;
}
}
/// Wraps another ProxySelector to avoid using proxy for loopback addresses.
private static final class ProxySelectorWrapper extends AbstractProxySelector {
private final ProxySelector source;
ProxySelectorWrapper(ProxySelector source) {
this.source = source;
}
@Override
@@ -122,19 +154,10 @@ public final class ProxyManager {
if (uri == null)
throw new IllegalArgumentException("URI can't be null.");
return proxies;
}
if (NetworkUtils.isLoopbackAddress(uri))
return NO_PROXY.proxies;
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (uri == null || sa == null || ioe == null) {
throw new IllegalArgumentException("Arguments can't be null.");
}
}
@Override
public String toString() {
return "SimpleProxySelector" + proxies;
return source.select(uri);
}
}
@@ -33,7 +33,8 @@ public enum VersionIconType {
FORGE("/assets/img/forge.png"),
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
QUILT("/assets/img/quilt.png"),
APRIL_FOOLS("/assets/img/april_fools.png");
// Please append new items at last
@@ -797,14 +797,6 @@ public final class VersionSetting implements Cloneable, Observable {
return obj;
}
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
if (index == null)
return defaultValue;
int idx = index.getAsInt();
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
}
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject))
@@ -839,14 +831,14 @@ public final class VersionSetting implements Cloneable, Observable {
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
vs.setGameDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("gameDirType"), GameDirectoryType.class, GameDirectoryType.ROOT_FOLDER));
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
vs.setNativesDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("nativesDirType"), NativesDirectoryType.class, NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(parseJsonPrimitive(obj.getAsJsonPrimitive("versionIcon"), VersionIconType.class, VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -216,7 +217,11 @@ public final class Controllers {
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()) {
&& !Controllers.stage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
) {
targetProperty.set(sourceProperty.get());
}
};
@@ -321,6 +326,27 @@ public final class Controllers {
}
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
Number shownTipVersion = null;
try {
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(
i18n("fatal.deprecated_java_version.download_link", Metadata.RECOMMENDED_JAVA_VERSION),
downloadLink
);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
.build());
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
@@ -25,10 +25,7 @@ import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.ObservableMap;
import javafx.event.Event;
@@ -832,10 +829,144 @@ public final class FXUtils {
property.addListener(binding);
}
public static void unbindColorPicker(ColorPicker colorPicker, Property<Paint> property) {
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
colorPicker.valueProperty().removeListener(binding);
property.removeListener(binding);
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
private final WeakReference<JFXComboBox<String>> comboBoxRef;
private final WeakReference<IntegerProperty> widthPropertyRef;
private final WeakReference<IntegerProperty> heightPropertyRef;
private final int hashCode;
private boolean updating = false;
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
IntegerProperty widthProperty,
IntegerProperty heightProperty) {
this.comboBoxRef = new WeakReference<>(comboBox);
this.widthPropertyRef = new WeakReference<>(widthProperty);
this.heightPropertyRef = new WeakReference<>(heightProperty);
this.hashCode = System.identityHashCode(comboBox)
^ System.identityHashCode(widthProperty)
^ System.identityHashCode(heightProperty);
}
@Override
public void invalidated(Observable observable) {
if (!updating) {
var comboBox = this.comboBoxRef.get();
var widthProperty = this.widthPropertyRef.get();
var heightProperty = this.heightPropertyRef.get();
if (comboBox == null || widthProperty == null || heightProperty == null) {
if (comboBox != null) {
comboBox.focusedProperty().removeListener(this);
comboBox.sceneProperty().removeListener(this);
}
if (widthProperty != null)
widthProperty.removeListener(this);
if (heightProperty != null)
heightProperty.removeListener(this);
} else {
updating = true;
try {
int width = widthProperty.get();
int height = heightProperty.get();
if (observable instanceof ReadOnlyProperty<?>
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
String value = comboBox.valueProperty().get();
if (value == null)
value = "";
int idx = value.indexOf('x');
if (idx < 0)
idx = value.indexOf('*');
if (idx < 0) {
LOG.warning("Bad window size: " + value);
comboBox.setValue(width + "x" + height);
return;
}
String widthStr = value.substring(0, idx).trim();
String heightStr = value.substring(idx + 1).trim();
int newWidth;
int newHeight;
try {
newWidth = Integer.parseInt(widthStr);
newHeight = Integer.parseInt(heightStr);
} catch (NumberFormatException e) {
LOG.warning("Bad window size: " + value);
comboBox.setValue(width + "x" + height);
return;
}
widthProperty.set(newWidth);
heightProperty.set(newHeight);
} else {
comboBox.setValue(width + "x" + height);
}
} finally {
updating = false;
}
}
}
}
@Override
public boolean wasGarbageCollected() {
return this.comboBoxRef.get() == null
|| this.widthPropertyRef.get() == null
|| this.heightPropertyRef.get() == null;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof WindowsSizeBidirectionalBinding))
return false;
var that = (WindowsSizeBidirectionalBinding) obj;
var comboBox = this.comboBoxRef.get();
var widthProperty = this.widthPropertyRef.get();
var heightProperty = this.heightPropertyRef.get();
var thatComboBox = that.comboBoxRef.get();
var thatWidthProperty = that.widthPropertyRef.get();
var thatHeightProperty = that.heightPropertyRef.get();
if (comboBox == null || widthProperty == null || heightProperty == null
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
return false;
}
return comboBox == thatComboBox
&& widthProperty == thatWidthProperty
&& heightProperty == thatHeightProperty;
}
}
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
comboBox.focusedProperty().addListener(binding);
comboBox.sceneProperty().addListener(binding);
widthProperty.addListener(binding);
heightProperty.addListener(binding);
}
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
comboBox.focusedProperty().removeListener(binding);
comboBox.sceneProperty().removeListener(binding);
widthProperty.removeListener(binding);
heightProperty.removeListener(binding);
}
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
@@ -30,8 +30,7 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private Duration duration;
private Node previousNode, currentNode;
{
currentNode = getChildren().stream().findFirst().orElse(null);
public TransitionPane() {
FXUtils.setOverflowHidden(this);
}
@@ -91,8 +90,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private void updateContent(Node newView) {
if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null)
previousNode = EMPTY_PANE;
if (previousNode == null) {
if (getChildren().isEmpty())
previousNode = EMPTY_PANE;
else
previousNode = getChildren().get(0);
}
} else
previousNode = EMPTY_PANE;
@@ -22,7 +22,6 @@ import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -59,16 +58,10 @@ public class TaskExecutorDialogPane extends BorderPane {
lblTitle = new Label();
lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
{
taskListPane = new TaskListPane();
scrollPane.setContent(taskListPane);
}
taskListPane = new TaskListPane();
VBox.setVgrow(taskListPane, Priority.ALWAYS);
center.getChildren().setAll(lblTitle, scrollPane);
center.getChildren().setAll(lblTitle, taskListPane);
}
BorderPane bottom = new BorderPane();
@@ -17,15 +17,20 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
@@ -52,6 +57,7 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthCompletionTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthInstallTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
@@ -63,54 +69,86 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.FXThread;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
private TaskExecutor executor;
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXListView<Node> listView = new JFXListView<>();
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
private final List<StageNode> stageNodes = new ArrayList<>();
private final Map<String, StageNode> stageNodes = new HashMap<>();
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
private final DoubleProperty cellWidth = new SimpleDoubleProperty();
private Cell lastCell;
public TaskListPane() {
listBox.setSpacing(0);
listView.setPadding(new Insets(12, 0, 0, 0));
listView.setCellFactory(l -> new Cell());
listView.setSelectionModel(null);
FXUtils.onChangeAndOperate(listView.widthProperty(), width -> {
double w = width.doubleValue();
cellWidth.set(w <= 12.0 ? w : w - 12.0);
});
getChildren().setAll(listBox);
getChildren().setAll(listView);
}
@FXThread
private void addStages(@NotNull Collection<String> stages) {
for (String stage : stages) {
stageNodes.computeIfAbsent(stage, s -> {
StageNode node = new StageNode(stage);
listView.getItems().add(node);
return node;
});
}
}
@FXThread
private void updateProgressNodePadding() {
progressNodePadding.set(stageNodes.isEmpty() ? DEFAULT_PROGRESS_NODE_PADDING : STAGED_PROGRESS_NODE_PADDING);
}
public void setExecutor(TaskExecutor executor) {
List<String> stages = Lang.removingDuplicates(executor.getStages());
this.executor = executor;
executor.addTaskListener(new TaskListener() {
@Override
public void onStart() {
Platform.runLater(() -> {
stageNodes.clear();
listBox.clear();
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
stageNodes.forEach(listBox::add);
if (stages.isEmpty()) progressNodePadding.setValue(new Insets(0, 0, 8, 0));
else progressNodePadding.setValue(new Insets(0, 0, 8, 26));
listView.getItems().clear();
addStages(executor.getStages());
updateProgressNodePadding();
});
}
@Override
public void onReady(Task<?> task) {
if (task instanceof Task.StagesHintTask) {
Platform.runLater(() -> {
addStages(((Task<?>.StagesHintTask) task).getStages());
updateProgressNodePadding();
});
}
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
StageNode node = stageNodes.get(task.getStage());
if (node != null)
node.begin();
});
}
}
@@ -123,6 +161,8 @@ public final class TaskListPane extends StackPane {
if (task instanceof GameAssetDownloadTask) {
task.setName(i18n("assets.download_all"));
} else if (task instanceof GameInstallTask) {
if (task.getInheritedStage() != null && task.getInheritedStage().startsWith("hmcl.install.game"))
return;
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
@@ -156,7 +196,7 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
@@ -169,25 +209,25 @@ public final class TaskListPane extends StackPane {
Platform.runLater(() -> {
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
listBox.add(listBox.indexOf(stageNode) + 1, node);
StageNode stageNode = stageNodes.get(task.getInheritedStage());
listView.getItems().add(listView.getItems().indexOf(stageNode) + 1, node);
});
}
@Override
public void onFinished(Task<?> task) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
});
}
Platform.runLater(() -> {
if (task.getStage() != null) {
StageNode stageNode = stageNodes.get(task.getStage());
if (stageNode != null)
stageNode.succeed();
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
listBox.remove(node);
if (node != null) {
node.unbind();
listView.getItems().remove(node);
}
});
}
@@ -195,25 +235,23 @@ public final class TaskListPane extends StackPane {
public void onFailed(Task<?> task, Throwable throwable) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
StageNode stageNode = stageNodes.get(task.getStage());
if (stageNode != null)
stageNode.fail();
});
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
Platform.runLater(() -> {
node.setThrowable(throwable);
});
if (node != null)
Platform.runLater(() -> node.setThrowable(throwable));
}
@Override
public void onPropertiesUpdate(Task<?> task) {
if (task instanceof Task.CountTask) {
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(((Task<?>.CountTask) task).getCountStage()))
.findAny()
.ifPresent(StageNode::count);
StageNode stageNode = stageNodes.get(((Task<?>.CountTask) task).getCountStage());
if (stageNode != null)
stageNode.count();
});
return;
@@ -222,121 +260,251 @@ public final class TaskListPane extends StackPane {
if (task.getStage() != null) {
int total = tryCast(task.getProperties().get("total"), Integer.class).orElse(0);
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(task.getStage()))
.findAny()
.ifPresent(stageNode -> {
stageNode.setTotal(total);
});
StageNode stageNode = stageNodes.get(task.getStage());
if (stageNode != null)
stageNode.addTotal(total);
});
}
}
});
}
private static class StageNode extends BorderPane {
private final String stage;
private final class Cell extends ListCell<Node> {
private static final double STATUS_ICON_SIZE = 14;
private final BorderPane pane = new BorderPane();
private final StackPane left = new StackPane();
private final Label title = new Label();
private final Label message = new Label();
private final JFXProgressBar bar = new JFXProgressBar();
private WeakReference<StageNode> prevStageNodeRef;
private StatusChangeListener statusChangeListener;
private Cell() {
setPadding(new Insets(0, 0, 4, 0));
prefWidthProperty().bind(cellWidth);
FXUtils.setLimitHeight(left, STATUS_ICON_SIZE);
FXUtils.setLimitWidth(left, STATUS_ICON_SIZE);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
BorderPane.setMargin(left, new Insets(0, 12, 0, 0));
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
pane.setCenter(title);
DoubleBinding barWidth = Bindings.createDoubleBinding(() -> {
Insets padding = pane.getPadding();
Insets insets = pane.getInsets();
return pane.getWidth() - padding.getLeft() - padding.getRight() - insets.getLeft() - insets.getRight();
}, pane.paddingProperty(), pane.widthProperty());
bar.minWidthProperty().bind(barWidth);
bar.prefWidthProperty().bind(barWidth);
bar.maxWidthProperty().bind(barWidth);
setGraphic(pane);
}
private void updateLeftIcon(StageNode.Status status) {
left.getChildren().setAll(status.svg.createIcon(Theme.blackFill(), STATUS_ICON_SIZE));
}
@Override
protected void updateItem(Node item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell && !isVisible())
return;
lastCell = this;
pane.paddingProperty().unbind();
title.textProperty().unbind();
message.textProperty().unbind();
bar.progressProperty().unbind();
StageNode prevStageNode;
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
prevStageNode.status.removeListener(statusChangeListener);
if (item instanceof ProgressListNode) {
var progressListNode = (ProgressListNode) item;
title.setText(progressListNode.title);
message.textProperty().bind(progressListNode.message);
bar.progressProperty().bind(progressListNode.progress);
pane.paddingProperty().bind(progressNodePadding);
pane.setLeft(null);
pane.setRight(message);
pane.setBottom(bar);
} else if (item instanceof StageNode) {
var stageNode = (StageNode) item;
title.textProperty().bind(stageNode.title);
message.setText("");
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
pane.setPadding(Insets.EMPTY);
pane.setLeft(left);
pane.setRight(message);
pane.setBottom(null);
updateLeftIcon(stageNode.status.get());
if (statusChangeListener == null)
statusChangeListener = new StatusChangeListener(this);
stageNode.status.addListener(statusChangeListener);
prevStageNodeRef = new WeakReference<>(stageNode);
} else { // item == null
title.setText("");
message.setText("");
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
pane.setPadding(Insets.EMPTY);
pane.setLeft(null);
pane.setRight(null);
pane.setBottom(null);
}
}
}
private static final class StatusChangeListener implements ChangeListener<StageNode.Status>, WeakListener {
private final WeakReference<Cell> cellRef;
private StatusChangeListener(Cell cell) {
this.cellRef = new WeakReference<>(cell);
}
@Override
public boolean wasGarbageCollected() {
return cellRef.get() == null;
}
@Override
public void changed(ObservableValue<? extends StageNode.Status> observable,
StageNode.Status oldValue,
StageNode.Status newValue) {
Cell cell = cellRef.get();
if (cell == null) {
if (observable != null)
observable.removeListener(this);
return;
}
cell.updateLeftIcon(newValue);
}
}
private static abstract class Node {
}
private static final class StageNode extends Node {
private enum Status {
WAITING(SVG.MORE_HORIZ),
RUNNING(SVG.ARROW_FORWARD),
SUCCESS(SVG.CHECK),
FAILED(SVG.CLOSE);
private final SVG svg;
Status(SVG svg) {
this.svg = svg;
}
}
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.WAITING);
private final StringProperty title = new SimpleStringProperty();
private final String message;
private int count = 0;
private int total = 0;
private boolean started = false;
public StageNode(String stage) {
this.stage = stage;
private StageNode(String stage) {
String stageKey;
String stageValue;
String stageKey = StringUtils.substringBefore(stage, ':');
String stageValue = StringUtils.substringAfter(stage, ':');
int idx = stage.indexOf(':');
if (idx >= 0) {
stageKey = stage.substring(0, idx);
stageValue = stage.substring(idx + 1);
} else {
stageKey = stage;
stageValue = null;
}
// CHECKSTYLE:OFF
// @formatter:off
switch (stageKey) {
case "hmcl.modpack": message = i18n("install.modpack"); break;
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
case "hmcl.install.assets": message = i18n("assets.download"); break;
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
case "hmcl.modpack": message = i18n("install.modpack"); break;
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
case "hmcl.install.assets": message = i18n("assets.download"); break;
case "hmcl.install.libraries": message = i18n("libraries.download"); break;
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
default: message = i18n(stageKey); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
// CHECKSTYLE:ON
title.setText(message);
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
setPadding(new Insets(0, 0, 8, 4));
setCenter(title);
setLeft(FXUtils.limitingSize(SVG.MORE_HORIZ.createIcon(Theme.blackFill(), 14), 14, 14));
title.set(message);
}
public void begin() {
if (started) return;
started = true;
setLeft(FXUtils.limitingSize(SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), 14), 14, 14));
private void begin() {
if (status.get() == Status.WAITING) {
status.set(Status.RUNNING);
}
}
public void fail() {
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14), 14, 14));
status.set(Status.FAILED);
}
public void succeed() {
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14), 14, 14));
status.set(Status.SUCCESS);
}
public void count() {
updateCounter(++count, total);
}
public void setTotal(int total) {
this.total = total;
public void addTotal(int n) {
this.total += n;
updateCounter(count, total);
}
public void updateCounter(int count, int total) {
if (total > 0)
title.setText(String.format("%s - %d/%d", message, count, total));
else
title.setText(message);
title.setValue(total > 0
? message + " - " + count + "/" + total
: message
);
}
}
private class ProgressListNode extends BorderPane {
private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label();
private final Label state = new Label();
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
getWidth() - getPadding().getLeft() - getPadding().getRight() - getInsets().getLeft() - getInsets().getRight(),
paddingProperty(), widthProperty());
private static final class ProgressListNode extends Node {
private final String title;
private final StringProperty message = new SimpleStringProperty("");
private final DoubleProperty progress = new SimpleDoubleProperty(0.0);
public ProgressListNode(Task<?> task) {
bar.progressProperty().bind(task.progressProperty());
title.setText(task.getName());
state.textProperty().bind(task.messageProperty());
setLeft(title);
setRight(state);
setBottom(bar);
bar.minWidthProperty().bind(binding);
bar.prefWidthProperty().bind(binding);
bar.maxWidthProperty().bind(binding);
paddingProperty().bind(progressNodePadding);
private ProgressListNode(Task<?> task) {
this.title = task.getName();
message.bind(task.messageProperty());
progress.bind(task.progressProperty());
}
public void unbind() {
bar.progressProperty().unbind();
state.textProperty().unbind();
message.unbind();
progress.unbind();
}
public void setThrowable(Throwable throwable) {
unbind();
state.setText(throwable.getLocalizedMessage());
bar.setProgress(0);
message.set(throwable.getLocalizedMessage());
progress.set(0.);
}
}
}
@@ -34,10 +34,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
@@ -61,6 +58,7 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -167,6 +165,17 @@ public class DecoratorController {
// press ESC to go back
onEscPressed(navigator, this::back);
// https://github.com/HMCL-dev/HMCL/issues/4290
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
// press F11 to toggle full screen
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
e.consume();
}
});
}
try {
// For JavaFX 12+
MouseButton button = MouseButton.valueOf("BACK");
@@ -18,8 +18,11 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -29,6 +32,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -43,6 +47,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class DecoratorSkin extends SkinBase<Decorator> {
private final StackPane root, parent;
@@ -50,6 +55,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
private final Stage primaryStage;
private final TransitionPane navBarPane;
private final InvalidationListener onWindowsStatusChange;
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
/**
@@ -80,9 +87,34 @@ public class DecoratorSkin extends SkinBase<Decorator> {
skinnable.getSnackbar().registerSnackbarContainer(parent);
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
// https://github.com/HMCL-dev/HMCL/issues/4290
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
onWindowsStatusChange = observable -> {
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
} else {
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
}
};
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
onWindowsStatusChange.invalidated(null);
} else {
onWindowsStatusChange = null;
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
}
shadowContainer.getChildren().setAll(parent);
root.getChildren().setAll(shadowContainer);
@@ -156,6 +188,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
BorderPane titleBar = new BorderPane();
titleContainer.getChildren().add(titleBar);
// https://github.com/HMCL-dev/HMCL/issues/4290
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
primaryStage.setMaximized(!primaryStage.isMaximized());
event.consume();
}
});
}
Rectangle buttonsContainerPlaceHolder = new Rectangle();
{
navBarPane = new TransitionPane();
@@ -85,6 +85,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
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) {
@@ -17,24 +17,17 @@
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import com.jfoenix.controls.*;
import javafx.beans.InvalidationListener;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
@@ -55,291 +48,67 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.*;
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.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
public final class VersionsPage extends Control implements WizardPage, Refreshable {
private final String gameVersion;
private final String libraryId;
private final String title;
private final Navigation navigation;
private final JFXListView<RemoteVersion> list;
private final JFXSpinner spinner;
private final StackPane failedPane;
private final StackPane emptyPane;
private final TransitionPane root;
private final JFXCheckBox chkRelease;
private final JFXCheckBox chkSnapshot;
private final JFXCheckBox chkOld;
private final ComponentList centrePane;
private final StackPane center;
private final VersionList<?> versionList;
private Task<?> executor;
private final Runnable callback;
private final HBox searchBar;
private final StringProperty queryString = new SimpleStringProperty();
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.title = title;
this.gameVersion = gameVersion;
this.libraryId = libraryId;
this.navigation = navigation;
HintPane hintPane = new HintPane();
hintPane.setText(i18n("sponsor.bmclapi"));
hintPane.getStyleClass().add("sponsor-pane");
FXUtils.onClicked(hintPane, this::onSponsor);
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
this.setTop(hintPane);
root = new TransitionPane();
BorderPane toolbarPane = new BorderPane();
JFXButton btnRefresh;
{
spinner = new JFXSpinner();
center = new StackPane();
center.setStyle("-fx-padding: 10;");
{
centrePane = new ComponentList();
centrePane.getStyleClass().add("no-padding");
{
HBox checkPane = new HBox();
checkPane.setSpacing(10);
{
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
chkRelease.setSelected(true);
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
chkOld = new JFXCheckBox(i18n("version.game.old"));
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld);
}
list = new JFXListView<>();
list.getStyleClass().add("jfx-list-view-float");
VBox.setVgrow(list, Priority.ALWAYS);
TransitionPane rightToolbarPane = new TransitionPane();
{
HBox refreshPane = new HBox();
refreshPane.setAlignment(Pos.CENTER_RIGHT);
btnRefresh = new JFXButton(i18n("button.refresh"));
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.setOnAction(e -> onRefresh());
JFXButton btnSearch = new JFXButton(i18n("search"));
btnSearch.getStyleClass().add("jfx-tool-bar-button");
btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1)));
searchBar = new HBox();
{
searchBar.setAlignment(Pos.CENTER);
searchBar.setPadding(new Insets(0, 5, 0, 0));
JFXTextField searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
JFXButton closeSearchBar = new JFXButton();
closeSearchBar.getStyleClass().add("jfx-tool-bar-button");
closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1)));
closeSearchBar.setOnAction(e -> {
searchField.clear();
rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE);
});
onEscPressed(searchField, closeSearchBar::fire);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> queryString.set(searchField.getText()));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
});
searchBar.getChildren().setAll(searchField, closeSearchBar);
btnSearch.setOnAction(e -> {
rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE);
searchField.requestFocus();
});
}
refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh));
rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE);
}
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
toolbarPane.setLeft(checkPane);
toolbarPane.setRight(rightToolbarPane);
centrePane.getContent().setAll(toolbarPane, list);
}
center.getChildren().setAll(centrePane);
}
failedPane = new StackPane();
failedPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.refresh"));
FXUtils.onClicked(label, this::onRefresh);
failedPane.getChildren().setAll(label);
}
emptyPane = new StackPane();
emptyPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.empty"));
FXUtils.onClicked(label, this::onBack);
emptyPane.getChildren().setAll(label);
}
}
this.setCenter(root);
versionList = downloadProvider.getVersionListById(libraryId);
boolean hasType = versionList.hasType();
chkRelease.setManaged(hasType);
chkRelease.setVisible(hasType);
chkSnapshot.setManaged(hasType);
chkSnapshot.setVisible(hasType);
chkOld.setManaged(hasType);
chkOld.setVisible(hasType);
if (hasType) {
centrePane.getContent().setAll(toolbarPane, list);
} else {
centrePane.getContent().setAll(list);
}
ComponentList.setVgrow(list, Priority.ALWAYS);
InvalidationListener listener = o -> {
List<RemoteVersion> versions = loadVersions();
String query = queryString.get();
if (!StringUtils.isBlank(query)) {
Predicate<RemoteVersion> predicate;
if (query.startsWith("regex:")) {
try {
Pattern pattern = Pattern.compile(query.substring("regex:".length()));
predicate = it -> pattern.matcher(it.getSelfVersion()).find();
} catch (Throwable e) {
LOG.warning("Illegal regular expression", e);
return;
}
} else {
String lowerQueryString = query.toLowerCase(Locale.ROOT);
predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString);
}
versions = versions.stream().filter(predicate).collect(Collectors.toList());
}
list.getItems().setAll(versions);
};
chkRelease.selectedProperty().addListener(listener);
chkSnapshot.selectedProperty().addListener(listener);
chkOld.selectedProperty().addListener(listener);
queryString.addListener(listener);
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1)));
Holder<RemoteVersionListCell> lastCell = new Holder<>();
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
FXUtils.onClicked(list, () -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
callback.run();
});
this.versionList = downloadProvider.getVersionListById(libraryId);
this.callback = callback;
refresh();
}
private List<RemoteVersion> loadVersions() {
return versionList.getVersions(gameVersion).stream()
.filter(it -> {
switch (it.getVersionType()) {
case RELEASE:
return chkRelease.isSelected();
case PENDING:
case SNAPSHOT:
return chkSnapshot.isSelected();
case OLD:
return chkOld.isSelected();
default:
return true;
}
})
.sorted().collect(Collectors.toList());
@Override
protected Skin<?> createDefaultSkin() {
return new VersionsPageSkin(this);
}
@Override
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE);
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
root.setContent(emptyPane, ContainerAnimations.FADE);
status.set(Status.LOADING);
Task<?> task = versionList.refreshAsync(gameVersion)
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
.whenComplete(Schedulers.javafx(), (items, exception) -> {
if (exception == null) {
versions.setAll(items);
status.set(Status.SUCCESS);
} else {
if (items.isEmpty()) {
chkRelease.setSelected(true);
chkSnapshot.setSelected(true);
chkOld.setSelected(true);
} else {
list.getItems().setAll(items);
}
root.setContent(center, ContainerAnimations.FADE);
LOG.warning("Failed to fetch versions list", exception);
status.set(Status.FAILED);
}
});
} else {
LOG.warning("Failed to fetch versions list", exception);
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
root.setContent(failedPane, ContainerAnimations.FADE);
});
}
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
});
executor.start();
task.start();
}
@Override
@@ -350,9 +119,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId);
// fixme
// if (executor != null)
// executor.cancel(true);
}
private void onRefresh() {
@@ -367,10 +133,24 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
}
private enum Status {
LOADING,
FAILED,
SUCCESS,
}
private enum VersionTypeFilter {
ALL,
RELEASE,
SNAPSHOTS,
APRIL_FOOLS,
OLD
}
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
final IconedTwoLineListItem content = new IconedTwoLineListItem();
final RipplerContainer ripplerContainer = new RipplerContainer(content);
final StackPane pane = new StackPane();
private final IconedTwoLineListItem content = new IconedTwoLineListItem();
private final RipplerContainer ripplerContainer = new RipplerContainer(content);
private final StackPane pane = new StackPane();
private final Holder<RemoteVersionListCell> lastCell;
@@ -401,7 +181,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
}
setGraphic(pane);
content.setTitle(remoteVersion.getSelfVersion());
content.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
if (remoteVersion.getReleaseDate() != null) {
content.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
} else {
@@ -409,22 +189,30 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
}
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
String wikiSuffix = getWikiUrlSuffix(remoteVersion.getGameVersion());
switch (versionType) {
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(VersionIconType.GRASS.getIcon());
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
break;
case PENDING:
case SNAPSHOT:
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(VersionIconType.COMMAND.getIcon());
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
content.getTags().setAll(i18n("version.game.april_fools"));
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(VersionIconType.COMMAND.getIcon());
}
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
content.setExternalLink(null);
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
break;
}
} else {
@@ -452,5 +240,285 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
content.setExternalLink(null);
}
}
private String getWikiUrlSuffix(String gameVersion) {
String id = gameVersion.toLowerCase(Locale.ROOT);
switch (id) {
case "0.30-1":
case "0.30-2":
case "c0.30_01c":
return i18n("wiki.version.game.search", "Classic_0.30");
case "in-20100206-2103":
return i18n("wiki.version.game.search", "Indev_20100206");
case "inf-20100630-1":
return i18n("wiki.version.game.search", "Infdev_20100630");
case "inf-20100630-2":
return i18n("wiki.version.game.search", "Alpha_v1.0.0");
case "1.19_deep_dark_experimental_snapshot-1":
return "1.19-exp1";
case "in-20100130":
return i18n("wiki.version.game.search", "Indev_0.31_20100130");
case "b1.6-tb3":
return i18n("wiki.version.game.search", "Beta_1.6_Test_Build_3");
case "1.14_combat-212796":
return i18n("wiki.version.game.search", "1.14.3_-_Combat_Test");
case "1.14_combat-0":
return i18n("wiki.version.game.search", "Combat_Test_2");
case "1.14_combat-3":
return i18n("wiki.version.game.search", "Combat_Test_3");
case "1_15_combat-1":
return i18n("wiki.version.game.search", "Combat_Test_4");
case "1_15_combat-6":
return i18n("wiki.version.game.search", "Combat_Test_5");
case "1_16_combat-0":
return i18n("wiki.version.game.search", "Combat_Test_6");
case "1_16_combat-1":
return i18n("wiki.version.game.search", "Combat_Test_7");
case "1_16_combat-2":
return i18n("wiki.version.game.search", "Combat_Test_7b");
case "1_16_combat-3":
return i18n("wiki.version.game.search", "Combat_Test_7c");
case "1_16_combat-4":
return i18n("wiki.version.game.search", "Combat_Test_8");
case "1_16_combat-5":
return i18n("wiki.version.game.search", "Combat_Test_8b");
case "1_16_combat-6":
return i18n("wiki.version.game.search", "Combat_Test_8c");
}
if (id.startsWith("1.0.0-rc2")) return "RC2";
if (id.startsWith("2.0")) return i18n("wiki.version.game.search", "2.0");
if (id.startsWith("b1.8-pre1")) return "Beta_1.8-pre1";
if (id.startsWith("b1.1-")) return i18n("wiki.version.game.search", "Beta_1.1");
if (id.startsWith("a1.1.0")) return "Alpha_v1.1.0";
if (id.startsWith("a1.0.14")) return "Alpha_v1.0.14";
if (id.startsWith("a1.0.13_01")) return "Alpha_v1.0.13_01";
if (id.startsWith("in-20100214")) return i18n("wiki.version.game.search", "Indev_20100214");
if (id.contains("experimental-snapshot")) {
return id.replace("_experimental-snapshot-", "-exp");
}
if (id.startsWith("inf-")) return id.replace("inf-", "Infdev_");
if (id.startsWith("in-")) return id.replace("in-", "Indev_");
if (id.startsWith("rd-")) return "pre-Classic_" + id;
if (id.startsWith("b")) return id.replace("b", "Beta_");
if (id.startsWith("a")) return id.replace("a", "Alpha_v");
if (id.startsWith("c")) return id.replace("c", "Classic_").replace("st", "SURVIVAL_TEST");
return i18n("wiki.version.game.search", id);
}
}
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
private final JFXListView<RemoteVersion> list;
private final TransitionPane transitionPane;
private final JFXSpinner spinner;
private final JFXTextField nameField;
private final JFXComboBox<VersionTypeFilter> categoryField = new JFXComboBox<>();
VersionsPageSkin(VersionsPage control) {
super(control);
BorderPane root = new BorderPane();
GridPane searchPane = new GridPane();
root.setTop(searchPane);
searchPane.getStyleClass().addAll("card");
BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10));
ColumnConstraints nameColumn = new ColumnConstraints();
nameColumn.setMinWidth(USE_PREF_SIZE);
ColumnConstraints column1 = new ColumnConstraints();
column1.setHgrow(Priority.ALWAYS);
ColumnConstraints column2 = new ColumnConstraints();
column2.setMaxWidth(150);
ColumnConstraints column3 = new ColumnConstraints();
if (control.versionList.hasType())
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
else
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
searchPane.setHgap(16);
searchPane.setVgap(10);
{
int rowIndex = 0;
{
nameField = new JFXTextField();
nameField.setPromptText(i18n("version.search.prompt"));
nameField.textProperty().addListener(o -> updateList());
if ("game".equals(control.libraryId)) {
categoryField.getItems().setAll(
VersionTypeFilter.ALL,
VersionTypeFilter.RELEASE,
VersionTypeFilter.SNAPSHOTS,
VersionTypeFilter.APRIL_FOOLS,
VersionTypeFilter.OLD
);
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
} else {
categoryField.getItems().setAll(
VersionTypeFilter.ALL,
VersionTypeFilter.RELEASE,
VersionTypeFilter.SNAPSHOTS
);
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
}
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
refreshButton.setOnAction(event -> control.onRefresh());
if (control.versionList.hasType()) {
searchPane.addRow(rowIndex++,
new Label(i18n("version.search")), nameField,
new Label(i18n("version.game.type")), categoryField,
refreshButton
);
} else {
searchPane.addRow(rowIndex++,
new Label(i18n("version.search")), nameField,
refreshButton
);
}
}
// {
// HBox actionsBox = new HBox(8);
// GridPane.setColumnSpan(actionsBox, 4);
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
//
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
// refreshButton.setOnAction(event -> control.onRefresh());
//
// actionsBox.getChildren().setAll(refreshButton);
//
// searchPane.addRow(rowIndex++, actionsBox);
// }
}
{
SpinnerPane spinnerPane = new SpinnerPane();
root.setCenter(spinnerPane);
transitionPane = new TransitionPane();
spinner = new JFXSpinner();
StackPane centerWrapper = new StackPane();
centerWrapper.setStyle("-fx-padding: 10;");
{
ComponentList centrePane = new ComponentList();
centrePane.getStyleClass().add("no-padding");
{
list = new JFXListView<>();
list.getStyleClass().add("jfx-list-view-float");
VBox.setVgrow(list, Priority.ALWAYS);
control.versions.addListener((InvalidationListener) o -> updateList());
Holder<RemoteVersionListCell> lastCell = new Holder<>();
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId));
FXUtils.onClicked(list, () -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem());
control.callback.run();
});
ComponentList.setVgrow(list, Priority.ALWAYS);
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
centrePane.getContent().setAll(list);
}
centerWrapper.getChildren().setAll(centrePane);
}
StackPane failedPane = new StackPane();
failedPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.refresh"));
FXUtils.onClicked(label, control::onRefresh);
failedPane.getChildren().setAll(label);
}
StackPane emptyPane = new StackPane();
emptyPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.empty"));
FXUtils.onClicked(label, control::onBack);
emptyPane.getChildren().setAll(label);
}
FXUtils.onChangeAndOperate(control.status, status -> {
if (status == Status.LOADING)
transitionPane.setContent(spinner, ContainerAnimations.FADE);
else if (status == Status.SUCCESS)
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
else // if (status == Status.FAILED)
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
});
root.setCenter(transitionPane);
}
this.getChildren().setAll(root);
}
private void updateList() {
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
if (filter != null)
versions = versions.filter(it -> {
RemoteVersion.Type versionType = it.getVersionType();
switch (filter) {
case RELEASE:
return versionType == RemoteVersion.Type.RELEASE;
case SNAPSHOTS:
return versionType == RemoteVersion.Type.SNAPSHOT
|| versionType == RemoteVersion.Type.PENDING;
case APRIL_FOOLS:
return versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
case OLD:
return versionType == RemoteVersion.Type.OLD;
case ALL:
default:
return true;
}
});
String nameQuery = nameField.getText();
if (!StringUtils.isBlank(nameQuery)) {
if (nameQuery.startsWith("regex:")) {
try {
Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length()));
versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find());
} catch (Throwable e) {
LOG.warning("Illegal regular expression: " + nameQuery, e);
}
} else {
String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT);
versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString));
}
}
//noinspection DataFlowIssue
list.getItems().setAll(versions.collect(Collectors.toList()));
list.scrollTo(0);
}
}
}
@@ -77,6 +77,10 @@ public final class ExportWizardProvider implements WizardProvider {
File tempModpack;
Task<?> exportTask;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public boolean doPreExecute() {
return true;
@@ -165,6 +169,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency = null;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
@@ -181,6 +189,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
VersionSetting vs = profile.getVersionSetting(version);
@@ -225,6 +237,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
@@ -241,6 +257,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
dependency = new ModrinthModpackExportTask(
@@ -31,6 +31,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
@@ -42,10 +43,16 @@ import javafx.scene.shape.Rectangle;
import javafx.scene.text.TextFlow;
import javafx.util.Duration;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
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.SVG;
@@ -61,16 +68,24 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.download.RemoteVersion.Type.RELEASE;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class MainPage extends StackPane implements DecoratorPage {
private static final String ANNOUNCEMENT = "announcement";
@@ -211,7 +226,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchButton.setPrefWidth(230);
launchButton.setPrefHeight(55);
//launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.setOnAction(e -> launch());
launchButton.setDefaultButton(true);
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
{
@@ -219,18 +233,34 @@ public final class MainPage extends StackPane implements DecoratorPage {
graphic.setAlignment(Pos.CENTER);
graphic.setTranslateX(-7);
graphic.setMaxWidth(200);
Label launchLabel = new Label(i18n("version.launch"));
Label launchLabel = new Label();
launchLabel.setStyle("-fx-font-size: 16px;");
Label currentLabel = new Label();
currentLabel.setStyle("-fx-font-size: 12px;");
currentLabel.textProperty().bind(Bindings.createStringBinding(() -> {
if (getCurrentGame() == null) {
return i18n("version.empty");
} else {
return getCurrentGame();
FXUtils.onChangeAndOperate(currentGameProperty(), new Consumer<>() {
private Tooltip tooltip;
@Override
public void accept(String currentGame) {
if (currentGame == null) {
launchLabel.setText(i18n("version.launch.empty"));
currentLabel.setText(null);
graphic.getChildren().setAll(launchLabel);
launchButton.setOnAction(e -> MainPage.this.launchNoGame());
if (tooltip == null)
tooltip = new Tooltip(i18n("version.launch.empty.tooltip"));
FXUtils.installFastTooltip(launchButton, tooltip);
} else {
launchLabel.setText(i18n("version.launch"));
currentLabel.setText(currentGame);
graphic.getChildren().setAll(launchLabel, currentLabel);
launchButton.setOnAction(e -> MainPage.this.launch());
if (tooltip != null)
Tooltip.uninstall(launchButton, tooltip);
}
}
}, currentGameProperty()));
graphic.getChildren().setAll(launchLabel, currentLabel);
});
launchButton.setGraphic(graphic);
}
@@ -319,7 +349,46 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
private void launch() {
Versions.launch(Profiles.getSelectedProfile());
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), null);
}
private void launchNoGame() {
DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
VersionList<?> versionList = downloadProvider.getVersionListById("game");
Holder<String> gameVersionHolder = new Holder<>();
Task<?> task = versionList.refreshAsync("")
.thenSupplyAsync(() -> versionList.getVersions("").stream()
.filter(it -> it.getVersionType() == RELEASE)
.sorted()
.findFirst()
.orElseThrow(() -> new IOException("No versions found")))
.thenComposeAsync(version -> {
Profile profile = Profiles.getSelectedProfile();
DefaultDependencyManager dependency = profile.getDependency();
String gameVersion = gameVersionHolder.value = version.getGameVersion();
return dependency.gameBuilder()
.name(gameVersion)
.gameVersion(gameVersion)
.buildAsync();
})
.whenComplete(any -> profile.getRepository().refreshVersions())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
profile.setSelectedVersion(gameVersionHolder.value);
launch();
} else if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
LOG.warning("Failed to install game", exception);
Controllers.dialog(StringUtils.getStackTrace(exception),
i18n("install.failed"),
MessageDialogPane.MessageType.WARNING);
}
});
Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL);
}
private void onMenu() {
@@ -23,6 +23,7 @@ import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -34,22 +35,30 @@ import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.tukaani.xz.XZInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class SettingsPage extends SettingsView {
@@ -61,6 +70,8 @@ public final class SettingsPage extends SettingsView {
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
// ====
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
@@ -123,22 +134,73 @@ public final class SettingsPage extends SettingsView {
@Override
protected void onExportLogs() {
// We cannot determine which file is JUL using.
// So we write all the logs to a new file.
thread(() -> {
Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
LOG.info("Exporting logs to " + logFile);
try (OutputStream output = Files.newOutputStream(logFile)) {
LOG.exportLogs(output);
Path outputFile;
try {
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
Path tempFile = Files.createTempFile("hmcl-decompress-log-", ".txt");
try (var tempChannel = FileChannel.open(tempFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
for (Path path : recentLogFiles) {
String extension = FileUtils.getExtension(path);
decompress:
if ("gz".equalsIgnoreCase(extension) || "xz".equalsIgnoreCase(extension)) {
try (InputStream fis = Files.newInputStream(path);
InputStream uncompressed = "gz".equalsIgnoreCase(extension)
? new GZIPInputStream(fis)
: new XZInputStream(fis)) {
uncompressed.transferTo(Channels.newOutputStream(tempChannel));
} catch (IOException e) {
LOG.warning("Failed to decompress log: " + path, e);
break decompress;
}
zos.putNextEntry(new ZipEntry(StringUtils.substringBeforeLast(FileUtils.getName(path), '.')));
Channels.newInputStream(tempChannel).transferTo(zos);
zos.closeEntry();
tempChannel.truncate(0);
continue;
}
zos.putNextEntry(new ZipEntry(FileUtils.getName(path)));
Files.copy(path, zos);
zos.closeEntry();
}
zos.putNextEntry(new ZipEntry("hmcl-latest.log"));
LOG.exportLogs(zos);
zos.closeEntry();
} finally {
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
}
}
}
} catch (IOException e) {
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
LOG.warning("Failed to export logs", e);
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
return;
}
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
FXUtils.showFileInExplorer(logFile);
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
FXUtils.showFileInExplorer(outputFile);
});
}
@@ -37,7 +37,9 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
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;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import java.util.Arrays;
@@ -49,6 +51,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class SettingsView extends StackPane {
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final OptionToggleButton disableAutoGameOptionsPane;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final ComponentSublist fileCommonLocationSublist;
protected final Label lblUpdate;
@@ -177,14 +180,28 @@ public abstract class SettingsView extends StackPane {
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
languagePane.setLeft(left);
SupportedLocale currentLocale = I18n.getLocale();
cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(I18n::getName));
cboLanguage.setConverter(stringConverter(locale -> {
if (locale.isSameLanguage(currentLocale) || locale == Locales.DEFAULT)
return locale.getDisplayName(currentLocale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
}));
FXUtils.setLimitWidth(cboLanguage, 300);
languagePane.setRight(cboLanguage);
settingsPane.getContent().add(languagePane);
}
{
disableAutoGameOptionsPane = new OptionToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
BorderPane debugPane = new BorderPane();
@@ -39,10 +39,12 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -68,7 +70,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -90,6 +91,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private int searchID = 0;
protected RemoteModRepository repository;
private final DownloadProvider downloadProvider;
private Runnable retrySearch;
@@ -101,6 +103,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
this.repository = repository;
this.callback = callback;
this.versionSelection = versionSelection;
this.downloadProvider = DownloadProviders.getDownloadProvider();
}
public ObservableList<Node> getActions() {
@@ -171,7 +174,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
: "";
}
}).thenApplyAsync(
gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (searchID != currentSearchID) {
return;
@@ -231,6 +234,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
private final JFXListView<RemoteMod> listView = new JFXListView<>();
protected ModDownloadListPageSkin(DownloadListPage control) {
super(control);
@@ -543,7 +547,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
LOG.debug("Icon: " + dataItem.getIconUrl());
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
}
}
@@ -60,6 +60,7 @@ public class VersionIconDialog extends DialogPane {
createIcon(VersionIconType.CHEST),
createIcon(VersionIconType.CHICKEN),
createIcon(VersionIconType.COMMAND),
createIcon(VersionIconType.APRIL_FOOLS),
createIcon(VersionIconType.OPTIFINE),
createIcon(VersionIconType.CRAFT_TABLE),
createIcon(VersionIconType.FABRIC),
@@ -26,12 +26,14 @@ import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Screen;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
@@ -93,8 +95,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private String versionId;
private final VBox rootPane;
private final JFXTextField txtWidth;
private final JFXTextField txtHeight;
private final JFXComboBox<String> cboWindowsSize;
private final JFXTextField txtServerIP;
private final ComponentList componentList;
private final JFXComboBox<LauncherVisibility> cboLauncherVisibility;
@@ -389,29 +390,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
BorderPane right = new BorderPane();
dimensionPane.setRight(right);
{
HBox hbox = new HBox();
right.setLeft(hbox);
hbox.setPrefWidth(210);
hbox.setSpacing(3);
hbox.setAlignment(Pos.CENTER);
BorderPane.setAlignment(hbox, Pos.CENTER);
{
txtWidth = new JFXTextField();
txtWidth.setPromptText("800");
txtWidth.setPrefWidth(100);
FXUtils.setValidateWhileTextChanged(txtWidth, true);
txtWidth.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
Label x = new Label("x");
txtHeight = new JFXTextField();
txtHeight.setPromptText("480");
txtHeight.setPrefWidth(100);
FXUtils.setValidateWhileTextChanged(txtHeight, true);
txtHeight.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
hbox.getChildren().setAll(txtWidth, x, txtHeight);
}
cboWindowsSize = new JFXComboBox<>();
cboWindowsSize.setPrefWidth(150);
right.setLeft(cboWindowsSize);
cboWindowsSize.setEditable(true);
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
cboWindowsSize.setPromptText("854x480");
cboWindowsSize.getItems().setAll(getSupportedResolutions());
chkFullscreen = new JFXCheckBox();
right.setRight(chkFullscreen);
@@ -419,6 +404,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
chkFullscreen.setAlignment(Pos.CENTER);
BorderPane.setAlignment(chkFullscreen, Pos.CENTER);
BorderPane.setMargin(chkFullscreen, new Insets(0, 0, 0, 7));
cboWindowsSize.disableProperty().bind(chkFullscreen.selectedProperty());
}
}
@@ -559,8 +546,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
// unbind data fields
if (lastVersionSetting != null) {
FXUtils.unbind(txtWidth, lastVersionSetting.widthProperty());
FXUtils.unbind(txtHeight, lastVersionSetting.heightProperty());
FXUtils.unbindWindowsSize(cboWindowsSize, lastVersionSetting.widthProperty(), lastVersionSetting.heightProperty());
maxMemory.unbindBidirectional(lastVersionSetting.maxMemoryProperty());
javaCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.javaDirProperty());
gameDirCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.gameDirProperty());
@@ -593,8 +579,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
javaVersionOption.valueProperty().unbind();
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
FXUtils.bindWindowsSize(cboWindowsSize, versionSetting.widthProperty(), versionSetting.heightProperty());
maxMemory.bindBidirectional(versionSetting.maxMemoryProperty());
javaCustomOption.bindBidirectional(versionSetting.javaDirProperty());
@@ -794,6 +779,28 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
private static List<String> getSupportedResolutions() {
int maxScreenWidth = 0;
int maxScreenHeight = 0;
for (Screen screen : Screen.getScreens()) {
Rectangle2D bounds = screen.getBounds();
int screenWidth = (int) (bounds.getWidth() * screen.getOutputScaleX());
int screenHeight = (int) (bounds.getHeight() * screen.getOutputScaleY());
maxScreenWidth = Math.max(maxScreenWidth, screenWidth);
maxScreenHeight = Math.max(maxScreenHeight, screenHeight);
}
List<String> resolutions = new ArrayList<>(List.of("854x480", "1280x720", "1600x900"));
if (maxScreenWidth >= 1920 && maxScreenHeight >= 1080) resolutions.add("1920x1080");
if (maxScreenWidth >= 2560 && maxScreenHeight >= 1440) resolutions.add("2560x1440");
if (maxScreenWidth >= 3840 && maxScreenHeight >= 2160) resolutions.add("3840x2160");
return resolutions;
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
@@ -205,14 +205,6 @@ public final class Versions {
});
}
public static void launch(Profile profile) {
launch(profile, profile.getSelectedVersion());
}
public static void launch(Profile profile, String id) {
launch(profile, id, null);
}
public static void launch(Profile profile, String id, Consumer<LauncherHelper> injecter) {
if (!checkVersionForLaunching(profile, id))
return;
@@ -361,14 +361,27 @@ public final class WorldInfoPage extends SpinnerPane {
playerGameTypePane.setRight(gameTypeBox);
Tag tag = player.get("playerGameType");
Tag hardcoreTag = dataTag.get("hardcore");
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
if (tag instanceof IntTag) {
IntTag intTag = (IntTag) tag;
GameType gameType = GameType.of(intTag.getValue());
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
if (gameType != null) {
gameTypeBox.setValue(gameType);
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
intTag.setValue(newValue.ordinal());
if (newValue == GameType.HARDCORE) {
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 1);
}
} else {
intTag.setValue(newValue.ordinal());
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 0);
}
}
saveLevelDat();
}
});
@@ -606,12 +619,13 @@ public final class WorldInfoPage extends SpinnerPane {
}
private enum GameType {
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR;
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR, HARDCORE;
static final ObservableList<GameType> items = FXCollections.observableList(Arrays.asList(values()));
static GameType of(int d) {
return d >= 0 && d <= items.size() ? items.get(d) : null;
static GameType of(int d, boolean hardcore) {
if (hardcore && d == 0) return HARDCORE; // hardcore + survival
return d >= 0 && d < 4 ? items.get(d) : null;
}
@Override
@@ -18,14 +18,9 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
final class HMCLDownloadTask extends FileDownloadTask {
@@ -45,15 +40,6 @@ final class HMCLDownloadTask extends FileDownloadTask {
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
@@ -21,14 +21,13 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Optional;
public class RemoteVersion {
public final class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
try {
@@ -36,12 +35,8 @@ public class RemoteVersion {
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash), force);
} else if (jarUrl != null && jarHash != null) {
if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
} else {
throw new IOException("No download url is available");
@@ -97,7 +92,6 @@ public class RemoteVersion {
}
public enum Type {
PACK_XZ,
JAR
}
}
@@ -30,9 +30,9 @@ public final class ChunkBaseApp {
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3", "1.19",
"1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12", "1.11",
"1.10", "1.9", "1.8", "1.7"
"1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3",
"1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12",
"1.11", "1.10", "1.9", "1.8", "1.7"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
@@ -26,7 +26,10 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.windows.WindowsVersion;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -178,7 +181,19 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static Library getMesaLoader(JavaRuntime javaVersion) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull WindowsVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.compareTo(WindowsVersion.WINDOWS_10) >= 0) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
} else if (windowsVersion.compareTo(WindowsVersion.WINDOWS_7) >= 0) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
else
return null;
} else {
return null;
}
}
}
@@ -46,7 +46,6 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.Platform;
@@ -63,6 +62,7 @@ import java.util.List;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Manifest;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
@@ -261,9 +261,15 @@ public final class SelfDependencyPatcher {
.map(DependencyDescriptor::localPath)
.toArray(Path[]::new);
String[] addOpens = JarUtils.getManifestAttribute("Add-Opens", "").split(" ");
String addOpens = null;
try (InputStream input = SelfDependencyPatcher.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
if (input != null)
addOpens = new Manifest(input).getMainAttributes().getValue("Add-Opens");
} catch (IOException e) {
LOG.warning("Failed to read MANIFEST.MF file", e);
}
JavaFXPatcher.patch(modules, jars, addOpens);
JavaFXPatcher.patch(modules, jars, addOpens != null ? addOpens.split(" ") : new String[0]);
}
/**
@@ -17,10 +17,9 @@
*/
package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.*;
@@ -33,26 +32,24 @@ public final class I18n {
private static volatile SupportedLocale locale = Locales.DEFAULT;
private static volatile ResourceBundle resourceBundle = locale.getResourceBundle();
private static volatile DateTimeFormatter dateTimeFormatter;
public static void setLocale(SupportedLocale locale) {
I18n.locale = locale;
resourceBundle = locale.getResourceBundle();
dateTimeFormatter = null;
}
public static SupportedLocale getLocale() {
return locale;
}
public static boolean isUseChinese() {
return locale.getLocale() == Locale.CHINA;
return locale.getLocale().getLanguage().equals("zh");
}
public static ResourceBundle getResourceBundle() {
return resourceBundle;
}
public static String getName(SupportedLocale locale) {
return locale == Locales.DEFAULT ? resourceBundle.getString("lang.default") : locale.getResourceBundle().getString("lang");
}
public static String i18n(String key, Object... formatArgs) {
try {
return String.format(getResourceBundle().getString(key), formatArgs);
@@ -75,12 +72,11 @@ public final class I18n {
}
public static String formatDateTime(TemporalAccessor time) {
DateTimeFormatter formatter = dateTimeFormatter;
if (formatter == null) {
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format")).withZone(ZoneId.systemDefault());
}
return locale.formatDateTime(time);
}
return formatter.format(time);
public static String getDisplaySelfVersion(RemoteVersion version) {
return locale.getDisplaySelfVersion(version);
}
public static boolean hasKey(String key) {
@@ -20,115 +20,224 @@ 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.Lang;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class Locales {
private Locales() {
}
public static final SupportedLocale DEFAULT = new SupportedLocale(Locale.getDefault());
public static final SupportedLocale DEFAULT = new SupportedLocale("def", Locale.getDefault()) {
@Override
public String getDisplayName(SupportedLocale inLocale) {
try {
return inLocale.getResourceBundle().getString("lang.default");
} catch (Throwable e) {
LOG.warning("Failed to get localized name for default locale", e);
return "Default";
}
}
};
/**
* English
*/
public static final SupportedLocale EN = new SupportedLocale(Locale.ROOT);
public static final SupportedLocale EN = new SupportedLocale("en", Locale.ENGLISH);
/**
* Spanish
*/
public static final SupportedLocale ES = new SupportedLocale(Locale.forLanguageTag("es"));
public static final SupportedLocale ES = new SupportedLocale("es", Locale.forLanguageTag("es"));
/**
* Russian
*/
public static final SupportedLocale RU = new SupportedLocale(Locale.forLanguageTag("ru"));
public static final SupportedLocale RU = new SupportedLocale("ru", Locale.forLanguageTag("ru"));
/**
* Ukrainian
*/
public static final SupportedLocale UK = new SupportedLocale("uk", Locale.forLanguageTag("uk"));
/**
* Japanese
*/
public static final SupportedLocale JA = new SupportedLocale(Locale.JAPANESE);
public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE);
/**
* Traditional Chinese
* Chinese (Simplified)
*/
public static final SupportedLocale ZH = new SupportedLocale(Locale.TRADITIONAL_CHINESE);
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", Locale.forLanguageTag("zh-Hans"));
/**
* Simplified Chinese
* Chinese (Traditional)
*/
public static final SupportedLocale ZH_CN = new SupportedLocale(Locale.SIMPLIFIED_CHINESE);
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ES, JA, RU, ZH_CN, ZH);
/**
* Wenyan (Classical Chinese)
*/
public static final SupportedLocale WENYAN = new SupportedLocale("lzh", Locale.forLanguageTag("lzh")) {
@Override
public String getDisplayName(SupportedLocale inLocale) {
if (isChinese(inLocale.locale))
return "文言";
String name = super.getDisplayName(inLocale);
return name.equals("lzh") || name.equals("Literary Chinese")
? "Classical Chinese"
: name;
}
@Override
public String formatDateTime(TemporalAccessor time) {
return WenyanUtils.formatDateTime(time);
}
@Override
public String getDisplaySelfVersion(RemoteVersion version) {
if (version instanceof GameRemoteVersion)
return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion()));
else
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
}
};
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;
switch (name.toLowerCase(Locale.ROOT)) {
case "en":
return EN;
case "es":
return ES;
case "ja":
return JA;
case "ru":
return RU;
for (SupportedLocale locale : LOCALES) {
if (locale.getName().equalsIgnoreCase(name))
return locale;
}
return DEFAULT;
}
public static boolean isEnglish(Locale locale) {
return locale.getLanguage().equals("en") || locale.getLanguage().isEmpty();
}
public static boolean isChinese(Locale locale) {
switch (locale.getLanguage()) {
case "zh":
return ZH;
case "zh_cn":
return ZH_CN;
case "lzh":
case "cmn":
return true;
default:
return DEFAULT;
return false;
}
}
public static String getNameByLocale(SupportedLocale locale) {
if (locale == EN) return "en";
else if (locale == ES) return "es";
else if (locale == RU) return "ru";
else if (locale == JA) return "ja";
else if (locale == ZH) return "zh";
else if (locale == ZH_CN) return "zh_CN";
else if (locale == DEFAULT) return "def";
else throw new IllegalArgumentException("Unknown locale: " + locale);
public static boolean isSimplifiedChinese(Locale locale) {
if (locale.getLanguage().equals("zh") || locale.getLanguage().equals("cmn")) {
String script = locale.getScript();
if (script.isEmpty()) {
String region = locale.getCountry();
return region.isEmpty() || region.equals("CN") || region.equals("SG") || region.equals("MY");
} else
return script.equals("Hans");
} else {
return false;
}
}
@JsonAdapter(SupportedLocale.TypeAdapter.class)
public static final class SupportedLocale {
public static class SupportedLocale {
private final String name;
private final Locale locale;
private ResourceBundle resourceBundle;
private DateTimeFormatter dateTimeFormatter;
SupportedLocale(Locale locale) {
SupportedLocale(String name, Locale locale) {
this.name = name;
this.locale = locale;
}
public String getName() {
return name;
}
public Locale getLocale() {
return locale;
}
public String getDisplayName(SupportedLocale inLocale) {
if (inLocale.locale.getLanguage().equals("lzh"))
inLocale = ZH_HANT;
return locale.getDisplayName(inLocale.getLocale());
}
public ResourceBundle getResourceBundle() {
ResourceBundle bundle = resourceBundle;
if (resourceBundle == null) {
if (this != DEFAULT && this.locale == DEFAULT.locale) {
bundle = DEFAULT.getResourceBundle();
} else {
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale);
}
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() {
@Override
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
if (isSimplifiedChinese(locale)) {
return List.of(
Locale.SIMPLIFIED_CHINESE,
Locale.CHINESE,
Locale.ROOT
);
}
resourceBundle = bundle;
if (locale.getLanguage().equals("lzh")) {
return List.of(
locale,
Locale.CHINESE,
Locale.ROOT
);
}
if (locale.getLanguage().equals("en")) {
return List.of(Locale.ROOT);
}
return super.getCandidateLocales(baseName, locale);
}
});
}
return bundle;
}
public String formatDateTime(TemporalAccessor time) {
DateTimeFormatter formatter = dateTimeFormatter;
if (formatter == null)
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
.withZone(ZoneId.systemDefault());
return formatter.format(time);
}
public String getDisplaySelfVersion(RemoteVersion version) {
return version.getSelfVersion();
}
public boolean isSameLanguage(SupportedLocale other) {
return this.getLocale().getLanguage().equals(other.getLocale().getLanguage())
|| isChinese(this.getLocale()) && isChinese(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(getNameByLocale(value));
out.value(value.getName());
}
@Override
@@ -0,0 +1,212 @@
/*
* 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 org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Glavo
*/
public final class WenyanUtils {
private static final String DOT = "點";
private static final String[] NUMBERS = {
"〇", "一", "二", "三", "四", "五", "六", "七", "八", "九",
"十", "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九",
"廿", "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九",
"卅", "卅一", "卅二", "卅三", "卅四", "卅五", "卅六", "卅七", "卅八", "卅九",
"卌", "卌一", "卌二", "卌三", "卌四", "卌五", "卌六", "卌七", "卌八", "卌九",
"五十", "五十一", "五十二", "五十三", "五十四", "五十五", "五十六", "五十七", "五十八", "五十九",
"六十", "六十一", "六十二", "六十三", "六十四", "六十五", "六十六", "六十七", "六十八", "六十九",
};
private static final char[] TIAN_GAN = {'甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'};
private static final char[] DI_ZHI = {'子', '醜', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'};
private static String digitToString(char digit) {
return digit >= '0' && digit <= '9'
? NUMBERS[digit - '0']
: String.valueOf(digit);
}
private static String numberToString(int number) {
return number >= 0 && number < NUMBERS.length ? NUMBERS[number] : String.valueOf(number);
}
private static void appendDigitByDigit(StringBuilder builder, String number) {
for (int i = 0; i < number.length(); i++) {
builder.append(digitToString(number.charAt(i)));
}
}
private static int mod(int a, int b) {
int r = a % b;
return r >= 0 ? r : r + b;
}
static void appendYear(StringBuilder builder, int year) {
int yearOffset = year - 1984;
builder.append(TIAN_GAN[mod(yearOffset, TIAN_GAN.length)]);
builder.append(DI_ZHI[mod(yearOffset, DI_ZHI.length)]);
}
public static String formatDateTime(TemporalAccessor time) {
LocalDateTime localDateTime;
if (time instanceof Instant)
localDateTime = ((Instant) time).atZone(ZoneId.systemDefault()).toLocalDateTime();
else
localDateTime = LocalDateTime.from(time);
StringBuilder builder = new StringBuilder(16);
appendYear(builder, localDateTime.getYear());
builder.append('年');
builder.append(numberToString(localDateTime.getMonthValue()));
builder.append('月');
builder.append(numberToString(localDateTime.getDayOfMonth()));
builder.append('日');
builder.append(' ');
builder.append(numberToString(localDateTime.getHour()));
builder.append('时');
builder.append(numberToString(localDateTime.getMinute()));
builder.append('分');
builder.append(numberToString(localDateTime.getSecond()));
builder.append('秒');
return builder.toString();
}
public static String translateGameVersion(GameVersionNumber gameVersion) {
if (gameVersion instanceof GameVersionNumber.Release) {
var release = (GameVersionNumber.Release) gameVersion;
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(release.getMajor()));
builder.append(DOT);
appendDigitByDigit(builder, String.valueOf(release.getMinor()));
if (release.getPatch() != 0) {
builder.append(DOT);
appendDigitByDigit(builder, String.valueOf(release.getPatch()));
}
if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) {
// do nothing
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) {
builder.append("之預");
appendDigitByDigit(builder, release.getEaVersion().toString());
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_RC) {
builder.append("之候");
appendDigitByDigit(builder, release.getEaVersion().toString());
} else {
// Unsupported
return gameVersion.toString();
}
return builder.toString();
} else if (gameVersion instanceof GameVersionNumber.Snapshot) {
var snapshot = (GameVersionNumber.Snapshot) gameVersion;
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(snapshot.getYear()));
builder.append('週');
appendDigitByDigit(builder, String.valueOf(snapshot.getWeek()));
char suffix = snapshot.getSuffix();
if (suffix >= 'a' && (suffix - 'a') < TIAN_GAN.length)
builder.append(TIAN_GAN[suffix - 'a']);
else
builder.append(suffix);
return builder.toString();
} else if (gameVersion instanceof GameVersionNumber.Special) {
String version = gameVersion.toString();
switch (version.toLowerCase(Locale.ROOT)) {
case "2.0":
return "二點〇";
case "2.0_blue":
return "二點〇藍";
case "2.0_red":
return "二點〇赤";
case "2.0_purple":
return "二點〇紫";
case "1.rv-pre1":
return "一點真視之預一";
case "3d shareware v1.34":
return "躍然享件一點三四";
case "20w14infinite":
case "20w14~":
case "20w14∞":
return "二〇週一四宇";
case "22w13oneblockatatime":
return "二二週一三典";
case "23w13a_or_b":
return "二三週一三暨";
case "24w14potato":
return "二四週一四芋";
case "25w14craftmine":
return "二五週一四礦";
default:
return version;
}
} else {
return gameVersion.toString();
}
}
private static final Pattern GENERIC_VERSION_PATTERN =
Pattern.compile("^[0-9]+(\\.[0-9]+)*");
public static String translateGenericVersion(String version) {
Matcher matcher = GENERIC_VERSION_PATTERN.matcher(version);
if (matcher.find()) {
String prefix = matcher.group();
StringBuilder builder = new StringBuilder(version.length());
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
if (ch >= '0' && ch <= '9')
builder.append(digitToString(ch));
else if (ch == '.')
builder.append(DOT);
else
builder.append(ch);
}
builder.append(version, prefix.length(), version.length());
return builder.toString();
}
return version;
}
private WenyanUtils() {
}
}
@@ -64,6 +64,12 @@
"titleLocalized" : "about.thanks_to.mcbbs",
"subtitleLocalized" : "about.thanks_to.mcbbs.statement"
},
{
"image" : "/assets/img/mcim.png",
"titleLocalized" : "about.thanks_to.mcim",
"subtitleLocalized" : "about.thanks_to.mcim.statement",
"externalLink" : "https://github.com/mcmod-info-mirror"
},
{
"image" : "/assets/img/github.png",
"titleLocalized" : "about.thanks_to.contributors",
+11 -11
View File
@@ -52,43 +52,43 @@
/*
Colors are picked from bootstrap
https://getbootstrap.com/docs/4.1/components/alerts/
https://getbootstrap.com/docs/5.3/components/alerts/
*/
.hint.info {
-fx-background-color: #cce5ff;
-fx-border-color: #b8daff;
-fx-background-color: #cfe2ff;
-fx-border-color: #9ec5fe;
}
.hint.info Text, .hint.info .svg {
-fx-fill: #004085;
-fx-fill: #052c65;
}
.hint.success {
-fx-background-color: #d4edda;
-fx-border-color: #c3e6cb;
-fx-background-color: #d1e7dd;
-fx-border-color: #a3cfbb;
}
.hint.success Text, .hint.success .svg {
-fx-fill: #155724;
-fx-fill: #0a3622;
}
.hint.error {
-fx-background-color: #f8d7da;
-fx-border-color: #f5c6cb;
-fx-border-color: #f1aeb5;
}
.hint.error Text, .hint.error .svg {
-fx-fill: #721c24;
-fx-fill: #58151c;
}
.hint.warning {
-fx-background-color: #fff3cd;
-fx-border-color: #ffeeba;
-fx-border-color: #ffe69c;
}
.hint.warning Text, .hint.warning .svg {
-fx-fill: #856404;
-fx-fill: #664d03;
}
.skin-pane .jfx-text-field {
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

@@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Contribute a lot of technical support to
about.thanks_to.zkitefly.statement=Responsible for maintaining the documentation of HMCL.
about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
about.thanks_to.mcbbs.statement=For providing the mcbbs.net download mirror for Chinese Mainland users. (No longer available)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=For providing the mod info cache acceleration service for Chinese Mainland users.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=For providing the Simplified Chinese translations and wiki for various mods.
about.thanks_to.red_lnn.statement=For providing the default background image.
@@ -398,6 +400,16 @@ fatal.apply_update_failure=We are sorry, but Hello Minecraft! Launcher is unable
fatal.apply_update_need_win7=Hello Minecraft! Launcher cannot automatically update on Windows XP/Vista.\n\
\n\
You can update manually by downloading a newer launcher version from %s.
fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the future, but will still support launching games with Java 6~16.\n\
\n\
It is recommended to install the latest version of Java to ensure that HMCL works properly.\n\
\n\
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
fatal.deprecated_java_version.update=HMCL will require Java 17 or later to run in the future. Please install the latest version of Java to ensure that HMCL can complete the upgrade.\n\
\n\
You can continue to keep the old version of Java.\
HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
fatal.deprecated_java_version.download_link=Download Java %d
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
@@ -435,7 +447,7 @@ folder.screenshots=Screenshots
folder.world=World Directory
game=Games
game.crash.feedback=<b>Please do not share screenshots of this window with others!</b> If you ask for help from others, please click <b>"Export Crash Logs"</b> and send the exported file to others for analysis.
game.crash.feedback=<b>Please do not share screenshots or photos of this interface with others!</b> If you ask for help from others, please click <b>"Export Crash Logs"</b> and send the exported file to others for analysis.
game.crash.info=Crash Info
game.crash.reason=Crash Cause
game.crash.reason.analyzing=Analyzing...
@@ -754,7 +766,6 @@ java.installing=Installing Java
java.uninstall=Uninstall Java
java.uninstall.confirm=Are you sure you want to uninstall this Java? This action cannot be undone!
lang=English
lang.default=Use System Locales
launch.advice=%s Do you still want to continue to launch?
@@ -825,6 +836,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a f
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher!
launcher.update_java=Please update your Java version.
libraries.download=Downloading Libraries
login.empty_username=You have not set your username yet!
login.enter_password=Please enter your password.
@@ -859,7 +872,7 @@ modpack.choose.local.detail=You can drag the modpack file here.
modpack.choose.remote=Download from URL
modpack.choose.remote.detail=A direct download link to the remote modpack file is required.
modpack.choose.repository=Download Modpack from CurseForge or Modrinth
modpack.choose.repository.detail=Remember to go back to this page and drop the modpack file here after the modpack is downloaded.
modpack.choose.repository.detail=You can choose the desired modpack on the next page.
modpack.choose.remote.tooltip=Please enter your modpack URL
modpack.completion=Downloading dependencies
modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported.
@@ -1136,6 +1149,7 @@ world.info.player.food_level=Hunger Level
world.info.player.game_type=Game Mode
world.info.player.game_type.adventure=Adventure
world.info.player.game_type.creative=Creative
world.info.player.game_type.hardcore=Hardcore
world.info.player.game_type.spectator=Spectator
world.info.player.game_type.survival=Survival
world.info.player.health=Health
@@ -1324,6 +1338,7 @@ settings.launcher=Launcher Settings
settings.launcher.appearance=Appearance
settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first.
settings.launcher.debug=Debug
settings.launcher.disable_auto_game_options=Do not switch game language
settings.launcher.download=Download
settings.launcher.download.threads=Threads
settings.launcher.download.threads.auto=Automatically Determine
@@ -1426,12 +1441,18 @@ version.empty=No Instances
version.empty.add=Add new instance
version.empty.launch=No available instances. Clicking "OK" will take you to the "Download" page.\n\nYou can also download the game or switch game directories via the "Download" or "All Instances" buttons on the HMCL homepage.
version.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to download one.
version.game.all=All
version.game.april_fools=April Fools
version.game.old=Historical
version.game.release=Release
version.game.releases=Releases
version.game.snapshot=Snapshot
version.game.snapshots=Snapshots
version.game.type=Type
version.launch=Launch Game
version.launch.empty=Start Game
version.launch.empty.installing=Installing Game
version.launch.empty.tooltip=Install and launch the latest official release
version.launch.test=Test Launch
version.switch=Switch Instance
version.launch_script=Export Launch Script
@@ -1456,12 +1477,14 @@ version.manage.remove_libraries=Delete All Libraries
version.manage.rename=Rename Instance
version.manage.rename.message=Enter New Instance Name
version.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character.
version.search=Name
version.search.prompt=Enter the version name to search
version.settings=Settings
version.update=Update Modpack
wiki.tooltip=Minecraft Wiki Page
wiki.version.game.release=https://minecraft.wiki/w/Java_Edition_%s
wiki.version.game.snapshot=https://minecraft.wiki/w/Java_Edition_%s
wiki.version.game=https://minecraft.wiki/w/Special:Search?search=%s
wiki.version.game.search=Java_Edition_%s
wizard.prev=< Prev
wizard.failed=Failed
@@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Aportar mucho apoyo técnico a HMCL.
about.thanks_to.zkitefly.statement=Responsable del mantenimiento de la documentación de HMCL.
about.thanks_to.mcbbs=MCBBS (Foro Chino Minecraft)
about.thanks_to.mcbbs.statement=Por proporcionar el espejo de descarga de mcbbs.net para los usuarios de China continental. (Ya no disponible)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=Por proporcionar el servicio de aceleración de caché de información de mods para los usuarios de China continental.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=Por proporcionar las traducciones al chino simplificado y la wiki de varios mods.
about.thanks_to.red_lnn.statement=Por proporcionar la imagen de fondo por defecto.
@@ -400,6 +402,17 @@ fatal.apply_update_failure=Lo sentimos, pero Hello Minecraft! Launcher no puede
fatal.apply_update_need_win7=Hello Minecraft! Launcher no puede actualizarse automáticamente en Windows XP/Vista.\n\
\n\
Puedes actualizar manualmente descargando una versión más reciente del launcher desde %s.
fatal.deprecated_java_version=HMCL requerirá Java 17 o posterior para funcionar en el futuro, pero seguirá siendo compatible con el lanzamiento de juegos con Java 6~16.\n\
\n\
Se recomienda instalar la última versión de Java para garantizar el correcto funcionamiento de HMCL.\n\
\n\
Puedes conservar la versión antigua de Java. HMCL puede reconocer y gestionar varias versiones de Java y elegirá automáticamente la adecuada para usted en función de la versión del juego.
fatal.deprecated_java_version.update=HMCL requerirá Java 17 o posterior para funcionar en el futuro. Por favor, instalar la última versión de Java para asegurarse de que el lanzador puede completar la actualización.\n\
\n\
Puedes conservar la versión antigua de Java.\
HMCL puede reconocer y gestionar varias versiones de Java \
y elegirá automáticamente la adecuada para usted en función de la versión del juego.
fatal.deprecated_java_version.download_link=Descargar Java %d
fatal.samba=Si ha abierto Hello Minecraft! Launcher desde una unidad de red Samba, es posible que algunas funciones no funcionen. Intenta actualizar tu Java o mover el launcher a otro directorio.
fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal «=», por lo que algunas características podrían no funcionar correctamente.\n\
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline.
@@ -438,7 +451,7 @@ folder.screenshots=Capturas de pantalla
folder.world=Directorio del mundo
game=Juegos
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla de esta interfaz con otras personas!</b> Si pide ayuda a otras personas, haga clic en <b>Exportar registros de colgado</b> y envíe el archivo exportado a otras personas para que lo analicen.
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla o fotos de esta interfaz con otras personas!</b> Si pide ayuda a otras personas, haga clic en <b>Exportar registros de colgado</b> y envíe el archivo exportado a otras personas para que lo analicen.
game.crash.info=Información de colgado
game.crash.reason=Motivo del colgado
game.crash.reason.analyzing=Analizando...
@@ -719,7 +732,7 @@ install.installer.external_version=%s (Instalado por un proceso externo, que no
install.installing=Instalando
install.modpack=Añadir modpack
install.modpack.installation=Instalación de modpack
install.name.invalid=El nombre contiene caracteres no ASCII (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
install.name.invalid=El nombre contiene caracteres especiales (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
install.new_game=Añadir instancia
install.new_game.already_exists=Esta instancia ya existe. Por favor, utilice otro nombre.
install.new_game.current_game_version=Versión actual de la instancia
@@ -759,7 +772,6 @@ java.installing=Instalando Java
java.uninstall=Desinstalar Java
java.uninstall.confirm=¿Está seguro de que desea desinstalar este Java? ¡Esta acción no se puede deshacer!
lang=Español
lang.default=Usar idioma del sistema
launch.advice=%s ¿Todavía quieres continuar?
@@ -830,6 +842,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher ha encontrado un er
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher ha encontrado un error fatal. Su launcher está desactualizado. Por favor, ¡actualícelo!
launcher.update_java=Por favor, actualice su versión de Java.
libraries.download=Descargando bibliotecas
login.empty_username=¡Todavía no has puesto tu nombre de usuario!
login.enter_password=Por favor, introduzca su contraseña.
@@ -865,7 +879,7 @@ modpack.choose.remote=Descargar desde Internet
modpack.choose.remote.detail=Se requiere un enlace de descarga directa al archivo modpack remoto
modpack.choose.remote.tooltip=Por favor, introduzca la URL de su modpack
modpack.choose.repository=Descargar Modpack de CurseForge o Modrinth
modpack.choose.repository.detail=Recuerda volver a esta página y soltar aquí el archivo del modpack una vez descargado.
modpack.choose.repository.detail=Puedes elegir el modpack que desees en la página siguiente.
modpack.completion=Descargando dependencias
modpack.desc=Describa su modpack, incluyendo una introducción y probablemente algún registro de cambios. Actualmente se admiten Markdown e imágenes desde URL.
modpack.description=Descripción
@@ -1426,12 +1440,18 @@ version.empty=No hay instancias
version.empty.add=Añadir una instancia
version.empty.launch=No hay instancias disponibles. Si haces clic en «Aceptar», accederás a la página «Descargar». También puedes descargar el juego o cambiar de directorio de juego con los botones «Descargar» o «Todas las instancias» de la página principal de HMCL.
version.empty.hint=No hay instancias de Minecraft aquí.\nPuedes intentar cambiar a otro directorio del juego o hacer clic aquí para descargar una.
version.game.all=Todos
version.game.april_fools=Día de los Inocentes
version.game.old=Histórico
version.game.release=Lanzamiento
version.game.releases=Lanzamientos
version.game.snapshot=Snapshot
version.game.snapshots=Snapshots
version.game.type=Tipo
version.launch=Iniciar Minecraft
version.launch.empty=Iniciar Minecraft
version.launch.empty.installing=Instalando el juego
version.launch.empty.tooltip=Instala y ejecuta la última versión oficial del juego.
version.launch.test=Probar instalación
version.switch=Cambiar Instancia
version.launch_script=Exportar script de ejecución
@@ -1456,12 +1476,14 @@ version.manage.remove_libraries=Borrar todas las bibliotecas
version.manage.rename=Renombrar instancia
version.manage.rename.message=Introduzca el nombre de la nueva instancia
version.manage.rename.fail=No se ha podido renombrar la instancia, algunos archivos pueden estar en uso o el nombre contiene un carácter no válido.
version.search=Nombre
version.search.prompt=Introduzca el nombre de la versión para buscar.
version.settings=Configuración
version.update=Actualizar Modpack
wiki.tooltip=Página de Minecraft Wiki
wiki.version.game.release=https://es.minecraft.wiki/w/Java_Edition_%s
wiki.version.game.snapshot=https://es.minecraft.wiki/w/Java_Edition_%s
wiki.version.game=https://es.minecraft.wiki/w/Special:Search?search=%s
wiki.version.game.search=Java_Edition_%s
wizard.prev=< Previo
wizard.failed=Falló
@@ -347,7 +347,7 @@ folder.saves=マップ
folder.screenshots=スクリーンショット
game=ゲーム
game.crash.feedback=<b>このインターフェースのスクリーンショットを他の人と共有しないでください。 </b> 他の人に助けを求める場合は、左下隅にあるゲーム クラッシュ情報をクリックしてエクスポートし、エクスポートされたファイルを分析のために他の人に送信してください。
game.crash.feedback=<b>このインターフェースのスクリーンショットや写真を他の人と共有しないでください。</b> 他の人に助けを求める場合は、左下隅にあるゲーム クラッシュ情報をクリックしてエクスポートし、エクスポートされたファイルを分析のために他の人に送信してください。
game.crash.info=ゲームステータス
game.crash.reason=クラッシュアナライザー
game.crash.reason.analyzing=分析中..
@@ -492,7 +492,6 @@ java.install.warning.invalid_character=名前に不正な文字が含まれて
java.uninstall=このJavaをアンインストールする
java.uninstall.confirm=本当にこのJavaをアンインストールしますか?この操作は元に戻せません!
lang=日本語
lang.default=システム言語を使用する
launch.advice.java.auto=現在選択されているJavaVMは、ゲームの要件を満たしていません。適切なJavaVMを選択することを許可しますか?または、ゲーム設定で適切なJavaVMを選択することもできます。
@@ -963,6 +962,9 @@ version.manage.rename.fail=バージョンの名前を変更できませんで
version.settings=設定
version.update=modpackを更新する
wiki.version.game=https://ja.minecraft.wiki/w/Special:Search?search=%s
wiki.version.game.search=Java_Edition_%s
wizard.prev=< 前へ
wizard.failed=失敗
wizard.finish=終了
File diff suppressed because it is too large. Load diff
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=Оказал большую технич
about.thanks_to.zkitefly.statement=Отвечает за поддержание документации HMCL.
about.thanks_to.mcbbs=MCBBS (Китайский форум Minecraft)
about.thanks_to.mcbbs.statement=За предоставление зеркала загрузки mcbbs.net для пользователей материкового Китая. (Больше недоступно)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=За предоставление услуги ускорения кэширования информации о модах для пользователей из материкового Китая.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=За предоставление упрощенного китайского перевода и вики для различных модов.
about.thanks_to.red_lnn.statement=За предоставление фонового изображения.
@@ -398,6 +400,17 @@ fatal.apply_update_failure=Мы сожалеем, лаунчер не смог
Вы можете обновить программу вручную, скачав более новую версию с %s.
fatal.apply_update_need_win7=Лаунчер не может автоматически обновляться на Windows XP/Vista.\n\
Вы можете обновить программу вручную, скачав более новую версию с %s.
fatal.deprecated_java_version=В будущем для работы HMCL потребуется Java 17 или более поздняя версия, однако запуск игр будет поддерживаться и на Java 6~16.\n\
\n\
Рекомендуется установить последнюю версию Java, чтобы обеспечить правильную работу HMCL.\n\
\n\
Вы можете продолжать использовать старую версию Java. HMCL может распознавать и управлять несколькими версиями Java и автоматически выбирать подходящую Java в зависимости от версии игры.
fatal.deprecated_java_version.update=В будущем для работы HMCL потребуется Java 17 или более поздняя версия. Пожалуйста, установить последнюю версию Java, чтобы убедиться, что приложение сможет завершить обновление.\n\
\n\
Вы можете продолжать использовать старую версию Java.\
HMCL может распознавать и управлять несколькими версиями Java \
и автоматически выбирать подходящую Java в зависимости от версии игры.
fatal.deprecated_java_version.download_link=Скачать Java %d
fatal.samba=Если вы пытаетесь открыть лаунчер в общей папке Samba, он может не работать, попробуйте обновить Java или запустить лаунчер из локальной папки.
fatal.illegal_char=Недопустимый символ «=» в пути к папке пользователя. Лаунчер может работать, но некоторые функции не будут работать.\n\
Вы не сможете использовать аккаунт authlib-injector или изменить скин для аккаунта в режиме офлайн.
@@ -436,7 +449,7 @@ folder.screenshots=Снимки экрана
folder.world=Папка мира
game=Игры
game.crash.feedback=<b>Пожалуйста, не делитесь скриншотами этого интерфейса с другими!</b> Если вы просите помощи у других, нажмите <b>«Экспорт журналов с ошибками»</b> и отправьте экспортированный файл другим для анализа.
game.crash.feedback=<b>Пожалуйста, не делитесь с другими скриншотами или фотографиями этого интерфейса.</b> Если вы просите помощи у других, нажмите <b>«Экспорт журналов с ошибками»</b> и отправьте экспортированный файл другим для анализа.
game.crash.info=Сведения об отказе
game.crash.reason=Анализатор сбоев
game.crash.reason.analyzing=Анализирование...
@@ -715,7 +728,7 @@ install.installer.external_version=%s (Устанавливается внешн
install.installing=Установка
install.modpack=Установить модпак
install.modpack.installation=Установка модпак
install.name.invalid=Имя содержит символы, отличные от ASCII (например, эмодзи или символы CJK).\nРекомендуется изменить имя, включив в него только английские буквы, цифры и подчеркивание, чтобы избежать возможных проблем при запуске игры.\nВы хотите продолжить процесс?
install.name.invalid=Имя содержит специальные символы (например, эмодзи или символы CJK).\nРекомендуется изменить имя, включив в него только английские буквы, цифры и подчеркивание, чтобы избежать возможных проблем при запуске игры.\nВы хотите продолжить процесс?
install.new_game=Установить сборку
install.new_game.already_exists=Это имя сборки уже существует. Пожалуйста, используйте другое имя.
install.new_game.current_game_version=Текущая версия сборки
@@ -755,7 +768,6 @@ java.installing=Установка Java
java.uninstall=Удалить Java
java.uninstall.confirm=Вы уверены, что хотите удалить эту Java? Это действие нельзя отменить!
lang=Русский
lang.default=Использовать язык системы
launch.advice=%s Вы все еще хотите продолжить запуск?
@@ -829,6 +841,8 @@ launcher.crash.java_internal_error=Лаунчер столкнулся с фат
launcher.crash.hmcl_out_dated=Лаунчер столкнулся с фатальной ошибкой! Ваш лаунчер устарел. Обновите его!
launcher.update_java=Пожалуйста, обновите версию Java.
libraries.download=Скачивание библиотек
login.empty_username=Имя пользователя не установлено!
login.enter_password=Введите пароль.
@@ -864,7 +878,7 @@ modpack.choose.remote=Скачать
modpack.choose.remote.detail=Необходима прямая ссылка на скачивание модпака
modpack.choose.remote.tooltip=Введите URL-адрес модпака
modpack.choose.repository=Скачать модпак с CurseForge или Modrinth
modpack.choose.repository.detail=Не забудьте вернуться на эту страницу и перетащить файл модпака сюда после того, как модпак будет скачан.
modpack.choose.repository.detail=Вы сможете выбрать нужный вам модпак на следующей странице.
modpack.completion=Скачивание зависимостей
modpack.desc=Опишите модпак, включая введение и журнал изменений. Поддерживаются Markdown и изображения по URL-адресу.
modpack.description=Описание
@@ -1426,12 +1440,18 @@ version.empty=Нет сборок
version.empty.add=Установить новую сборок
version.empty.launch=Нет доступных сборок. Нажав «ОК», вы перейдете на страницу «Скачать»\n\nВы также можете скачать игру или переключить директорию игры с помощью кнопок «Скачать» или «Все сборок» на главной странице HMCL.
version.empty.hint=Нет сборок.\nВы можете попробовать перейти в другую папку с игрой или нажать здесь, чтобы скачать игру.
version.game.all=Все
version.game.april_fools=День смеха
version.game.old=Исторические
version.game.release=Релиз
version.game.releases=Релизы
version.game.snapshot=Снапшот
version.game.snapshots=Снапшоты
version.game.type=Тип
version.launch=Играть
version.launch.empty=Играть
version.launch.empty.installing=Установка игры
version.launch.empty.tooltip=Установить и запустить последнюю официальную версию игры.
version.launch.test=Тестовая игра
version.switch=Сменить сборку
version.launch_script=Создать сценарий запуска
@@ -1456,12 +1476,14 @@ version.manage.remove_libraries=Удалить файлы библиотек
version.manage.rename=Переименовать
version.manage.rename.message=Введите новое название для этой сборки
version.manage.rename.fail=Не удалось переименовать сборку. Возможно, некоторые файлы уже используются или в их имени содержится недопустимый символ.
version.search=Название
version.search.prompt=Введите название версии для поиска
version.settings=Настройки
version.update=Обновить модпак
wiki.tooltip=Страница Minecraft Wiki
wiki.version.game.release=https://ru.minecraft.wiki/w/%s_(Java_Edition)
wiki.version.game.snapshot=https://ru.minecraft.wiki/w/%s_(Java_Edition)
wiki.version.game=https://ru.minecraft.wiki/w/Special:Search?search=%s
wiki.version.game.search=%s_(Java_Edition)
wizard.prev=< Пред.
wizard.failed=Не удалось
File diff suppressed because it is too large. Load diff
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=為 HMCL 貢獻許多技術支援
about.thanks_to.zkitefly.statement=負責維護 HMCL 的文件
about.thanks_to.mcbbs=MCBBS (我的世界中文論壇)
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 (現已停止服務)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=為中國大陸使用者提供模組資訊快取加速服務
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
about.thanks_to.mcmod.statement=提供模組簡體中文名映射表與模組百科
about.thanks_to.red_lnn.statement=提供預設背景圖
@@ -380,6 +382,9 @@ fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Laun
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
fatal.apply_update_need_win7=Hello Minecraft! Launcher 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。建議安裝最新版本的 Java 以確保 HMCL 正常執行。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能執行,請安裝最新版本的 Java 以確保 HMCL 能夠完成升級。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
fatal.deprecated_java_version.download_link=下載 Java %d
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
fatal.illegal_char=由於您的使用者目錄路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
fatal.unsupported_platform=Minecraft 尚未你您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「軟繪製器」,以獲得更好的相容性。
@@ -410,7 +415,7 @@ folder.screenshots=截圖目錄
folder.world=世界目錄
game=遊戲
game.crash.feedback=<b>請不要將本介面截圖給他人!</b>如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。
game.crash.feedback=<b>請不要將本介面截圖或拍照給他人!</b>如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。
game.crash.info=遊戲訊息
game.crash.reason=崩潰原因
game.crash.reason.analyzing=分析中……
@@ -570,7 +575,6 @@ java.installing=安裝 Java
java.uninstall=移除此 Java
java.uninstall.confirm=你確定要移除此 Java 嗎?此操作無法復原!
lang=繁體中文
lang.default=使用系統語言
launch.advice=%s 是否繼續啟動?
@@ -641,6 +645,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher 由於目前 Java
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
launcher.update_java=請更新你的 Java
libraries.download=下載依賴庫
login.empty_username=你還未設定使用者名稱!
login.enter_password=請輸入你的密碼
@@ -675,7 +681,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以
modpack.choose.remote=從網路下載模組包
modpack.choose.remote.detail=需要提供模組包的下載連結
modpack.choose.repository=從 CurseForge/Modrinth 下載模組包
modpack.choose.repository.detail=下載後記得回到這個介面,把模組包拖進來哦
modpack.choose.repository.detail=可直接在跳轉到的頁面安裝模組包
modpack.choose.remote.tooltip=要下載的模組包的連結
modpack.completion=下載模組包相關檔案
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新紀錄。支援 Markdown (圖片請上傳至網路)。
@@ -941,6 +947,7 @@ world.info.player.food_level=饑餓值
world.info.player.game_type=遊戲模式
world.info.player.game_type.adventure=冒險
world.info.player.game_type.creative=創造
world.info.player.game_type.hardcore=極限
world.info.player.game_type.spectator=旁觀者
world.info.player.game_type.survival=生存
world.info.player.health=生命值
@@ -1121,6 +1128,7 @@ settings.launcher=啟動器設定
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元件庫檔案放於此集中管理。如果遊戲目錄內有現成的將不會使用公共庫檔案。
settings.launcher.debug=除錯
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
settings.launcher.download=下載
settings.launcher.download.threads=執行緒數
settings.launcher.download.threads.auto=自動選取執行緒數
@@ -1219,12 +1227,18 @@ version.empty=沒有遊戲實例
version.empty.add=進入下載頁安裝遊戲
version.empty.launch=沒有可啟動的遊戲。點擊「確定」將進入「下載」頁面。\n你也可以點擊 HMCL 主介面左側的「下載」按鈕安裝遊戲,或在「實例清單」切換遊戲目錄。
version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處進入遊戲下載頁面。
version.game.all=全部
version.game.april_fools=愚人節
version.game.old=遠古版
version.game.release=正式版
version.game.releases=正式版
version.game.snapshot=快照
version.game.snapshots=快照
version.game.type=版本類型
version.launch=啟動遊戲
version.launch.empty=開始遊戲
version.launch.empty.installing=安裝遊戲
version.launch.empty.tooltip=安裝並啟動最新正式版遊戲
version.launch.test=測試遊戲
version.switch=切換實例
version.launch_script=生成啟動指令碼
@@ -1249,12 +1263,14 @@ version.manage.remove_libraries=刪除所有支援庫檔案
version.manage.rename=重新命名該實例
version.manage.rename.message=請輸入新名稱
version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。
version.search=名稱
version.search.prompt=輸入版本名稱進行搜尋
version.settings=遊戲設定
version.update=更新模組包
wiki.tooltip=Minecraft Wiki 頁面
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-tw
wiki.version.game.snapshot=https://zh.minecraft.wiki/w/%s?variant=zh-tw
wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-tw
wiki.version.game.search=Java版%s
wizard.prev=< 上一步
wizard.failed=失敗
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=为 HMCL 贡献许多技术支持
about.thanks_to.zkitefly.statement=负责维护 HMCL 的文档
about.thanks_to.mcbbs=MCBBS (我的世界中文论坛)
about.thanks_to.mcbbs.statement=提供 MCBBS 下载源 (现已停止服务)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=为中国大陆用户提供模组信息缓存加速服务
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
about.thanks_to.mcmod.statement=提供模组简体中文名映射表与模组百科
about.thanks_to.red_lnn.statement=提供默认背景图
@@ -389,6 +391,9 @@ fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Laun
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.apply_update_need_win7=Hello Minecraft! Launcher 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。建议安装最新版本的 Java 以确保 HMCL 能正常工作。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能运行,请安装最新版本的 Java 以确保 HMCL 能够完成升级。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。
fatal.deprecated_java_version.download_link=下载 Java %d
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/版本特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“软渲染器”,以获得更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -419,7 +424,7 @@ folder.screenshots=截图文件夹
folder.world=世界文件夹
game=游戏
game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要向他人求助,请你点击左下角<b>“导出游戏崩溃信息”</b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b>“帮助”</b>前往交流群寻求帮助。
game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如果你要向他人求助,请你点击左下角<b>“导出游戏崩溃信息”</b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b>“帮助”</b>前往交流群寻求帮助。
game.crash.info=游戏信息
game.crash.reason=崩溃原因
game.crash.reason.analyzing=分析中……
@@ -580,7 +585,6 @@ java.installing=安装 Java
java.uninstall=卸载此 Java
java.uninstall.confirm=你确定要卸载此 Java 吗?此操作无法撤销!
lang=简体中文
lang.default=跟随系统语言
launch.advice=%s 是否继续启动?
@@ -651,6 +655,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher 由于当前 Java
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
libraries.download=下载依赖库
login.empty_username=你还未设置用户名!
login.enter_password=请输入你的密码
@@ -685,7 +691,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
modpack.choose.remote=从互联网下载整合包
modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.repository=从 CurseForge/Modrinth 下载整合包
modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦
modpack.choose.repository.detail=可直接在跳转到的页面安装整合包
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.completion=下载整合包相关文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录。支持 Markdown (图片请用网络链接)。
@@ -952,6 +958,7 @@ world.info.player.food_level=饥饿值
world.info.player.game_type=游戏模式
world.info.player.game_type.adventure=冒险
world.info.player.game_type.creative=创造
world.info.player.game_type.hardcore=极限
world.info.player.game_type.spectator=旁观者
world.info.player.game_type.survival=生存
world.info.player.health=生命值
@@ -1131,6 +1138,7 @@ settings.launcher=启动器设置
settings.launcher.appearance=外观
settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件存放于此集中管理。如果游戏文件夹内有现成的将不会使用公共库文件。
settings.launcher.debug=调试
settings.launcher.disable_auto_game_options=不自动切换游戏语言
settings.launcher.download=下载
settings.launcher.download.threads=线程数
settings.launcher.download.threads.auto=自动选择线程数
@@ -1229,12 +1237,18 @@ version.empty=没有游戏版本
version.empty.add=进入下载页安装游戏
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。
version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。
version.game.all=全部
version.game.april_fools=愚人节
version.game.old=远古版
version.game.release=正式版
version.game.releases=正式版
version.game.snapshot=快照
version.game.snapshots=快照
version.game.type=版本类型
version.launch=启动游戏
version.launch.empty=开始游戏
version.launch.empty.installing=安装游戏
version.launch.empty.tooltip=安装并启动最新正式版游戏
version.launch.test=测试游戏
version.switch=切换版本
version.launch_script=生成启动脚本
@@ -1259,12 +1273,14 @@ version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本
version.manage.rename.message=请输入要修改的名称
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
version.search=名称
version.search.prompt=输入版本名称进行搜索
version.settings=游戏设置
version.update=更新整合包
wiki.tooltip=Minecraft Wiki 页面
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-cn
wiki.version.game.snapshot=https://zh.minecraft.wiki/w/%s?variant=zh-cn
wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-cn
wiki.version.game.search=Java版%s
wizard.prev=< 上一步
wizard.failed=失败
@@ -4007,6 +4007,17 @@
"size": 27971214
}
}
},
"software-renderer-loader": {
"name": "org.glavo:llvmpipe-loader:1.0",
"downloads": {
"artifact": {
"path": "org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
"sha1": "ff255415e5c4b2a18970da0a8e552b557ca013ae",
"size": 12964773
}
}
}
},
"windows-x86": {
@@ -0,0 +1,42 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Glavo
*/
public final class WenyanUtilsTest {
private static void assertYearToString(String value, int year) {
StringBuilder builder = new StringBuilder(2);
WenyanUtils.appendYear(builder, year);
assertEquals(value, builder.toString());
}
@Test
public void testYearToString() {
assertYearToString("甲子", 1984);
assertYearToString("乙巳", 2025);
assertYearToString("甲子", -2996);
assertYearToString("庚子", 1000);
}
}
@@ -0,0 +1,78 @@
/*
* 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.auth.authlibinjector;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class AuthlibInjectorExtractor implements AuthlibInjectorArtifactProvider {
private final URL source;
private final Path artifactLocation;
public AuthlibInjectorExtractor(URL source, Path artifactLocation) {
if (source == null)
throw new IllegalArgumentException("Missing authlib injector");
this.source = source;
this.artifactLocation = artifactLocation;
}
@Override
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
Optional<AuthlibInjectorArtifactInfo> cached = getArtifactInfoImmediately();
if (cached.isPresent())
return cached.get();
synchronized (this) {
cached = getArtifactInfoImmediately();
if (cached.isPresent())
return cached.get();
LOG.info("No local authlib-injector found, extracting");
Files.createDirectories(artifactLocation.getParent());
try (InputStream inputStream = source.openStream()) {
FileUtils.saveSafely(artifactLocation, inputStream::transferTo);
}
return getArtifactInfoImmediately().orElseThrow(() ->
new IOException("Failed to extract authlib-injector artifact"));
}
}
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
if (!Files.isRegularFile(artifactLocation))
return Optional.empty();
try {
return Optional.of(AuthlibInjectorArtifactInfo.from(artifactLocation));
} catch (IOException e) {
LOG.warning("Bad authlib-injector artifact", e);
return Optional.empty();
}
}
}
@@ -79,6 +79,12 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
pair("https://authlib-injector.yushi.moe", apiRoot + "/mirrors/authlib-injector"),
pair("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto")
// // https://github.com/mcmod-info-mirror/mcim-rust-api
// pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"),
// pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"),
// pair("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge"),
// pair("https://edge.forgecdn.net", "https://mod.mcimirror.top"),
// pair("https://mediafilez.forgecdn.net", "https://mod.mcimirror.top")
);
}
@@ -71,7 +71,7 @@ public class DefaultCacheRepository extends CacheRepository {
} else {
index = new Index();
}
} catch (IOException | JsonParseException e) {
} catch (Exception e) {
LOG.warning("Unable to read index file", e);
index = new Index();
} finally {
@@ -85,7 +85,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
else
return null;
}).thenComposeAsync(checkPatchCompletionAsync(version, integrityCheck)),
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck),
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck)
.setSignificance(Task.TaskSignificance.MODERATE),
new GameLibrariesTask(this, version, integrityCheck)
);
}
@@ -48,6 +48,7 @@ public class DefaultGameBuilder extends GameBuilder {
Task<Version> libraryTask = Task.supplyAsync(() -> new Version(name));
libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, "game", gameVersion));
stages.add("hmcl.install.game:" + gameVersion);
stages.add("hmcl.install.libraries");
stages.add("hmcl.install.assets");
for (Map.Entry<String, String> entry : toolVersions.entrySet()) {
@@ -75,6 +75,7 @@ public final class GameLibrariesTask extends Task<Void> {
this.integrityCheck = integrityCheck;
this.libraries = libraries;
setStage("hmcl.install.libraries");
setSignificance(TaskSignificance.MODERATE);
}
@@ -115,6 +116,7 @@ public final class GameLibrariesTask extends Task<Void> {
@Override
public void execute() throws IOException {
int progress = 0;
GameRepository gameRepository = dependencyManager.getGameRepository();
for (Library library : libraries) {
if (!library.appliesToCurrentEnvironment()) {
@@ -135,10 +137,17 @@ public final class GameLibrariesTask extends Task<Void> {
}
}
if (shouldDownloadLibrary(gameRepository, version, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))) {
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library).withCounter("hmcl.install.libraries"));
} else {
dependencyManager.getCacheRepository().tryCacheLibrary(library, file.toPath());
}
updateProgress(++progress, libraries.size());
}
if (!dependencies.isEmpty()) {
getProperties().put("total", dependencies.size());
notifyPropertiesChanged();
}
}
@@ -54,15 +54,16 @@ public final class GameRemoteVersion extends RemoteVersion {
@Override
public int compareTo(RemoteVersion o) {
if (!(o instanceof GameRemoteVersion))
if (!(o instanceof GameRemoteVersion)) {
return 0;
}
int dateCompare = o.getReleaseDate().compareTo(getReleaseDate());
if (dateCompare != 0) {
return dateCompare;
}
return GameVersionNumber.compare(getSelfVersion(), o.getSelfVersion());
return GameVersionNumber.compare(o.getSelfVersion(), getSelfVersion());
}
private static Type getReleaseType(ReleaseType type) {
@@ -42,7 +42,7 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
@Override
public boolean hasType() {
return false;
return true;
}
@Override
@@ -20,7 +20,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
@Override
public boolean hasType() {
return false;
return true;
}
private static final String OLD_URL = "https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge";
@@ -10,7 +10,7 @@ import java.util.List;
public class NeoForgeRemoteVersion extends RemoteVersion {
public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List<String> urls) {
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, urls);
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, getType(selfVersion), urls);
}
@Override
@@ -18,6 +18,10 @@ public class NeoForgeRemoteVersion extends RemoteVersion {
return new NeoForgeInstallTask(dependencyManager, baseVersion, this);
}
private static Type getType(String version) {
return version.contains("beta") ? Type.SNAPSHOT : Type.RELEASE;
}
public static String normalize(String version) {
if (version.startsWith("1.20.1-")) {
if (version.startsWith("forge-", "1.20.1-".length())) {
@@ -168,7 +168,7 @@ public class DefaultLauncher extends Launcher {
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
res.addDefault("-Duser.home=", options.getGameDir().getParent());
res.addDefault("-Duser.home=", options.getGameDir().getAbsoluteFile().getParent());
Proxy.Type proxyType = options.getProxyType();
if (proxyType == null) {
@@ -442,7 +442,7 @@ public class DefaultLauncher extends Launcher {
pair("${library_directory}", repository.getLibrariesDirectory(version).getAbsolutePath()),
pair("${classpath_separator}", File.pathSeparator),
pair("${primary_jar}", repository.getVersionJar(version).getAbsolutePath()),
pair("${language}", Locale.getDefault().toString()),
pair("${language}", Locale.getDefault().toLanguageTag()),
// defined by HMCL
// libraries_directory stands for historical reasons here. We don't know the official launcher
@@ -300,11 +300,13 @@ public final class RemoteMod {
public FileDownloadTask.IntegrityCheck getIntegrityCheck() {
if (hashes.containsKey("md5")) {
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("sha1"));
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("md5"));
} else if (hashes.containsKey("sha1")) {
return new FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1"));
} else if (hashes.containsKey("sha256")) {
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha256"));
} else if (hashes.containsKey("sha512")) {
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha1"));
return new FileDownloadTask.IntegrityCheck("SHA-512", hashes.get("sha512"));
} else {
return null;
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -83,7 +84,7 @@ public interface RemoteModRepository {
}
}
SearchResult search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
throws IOException;
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.mod.curse;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
@@ -26,6 +27,7 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
@@ -43,10 +45,17 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
private static final String PREFIX = "https://api.curseforge.com";
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getManifestAttribute("CurseForge-Api-Key", ""));
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
private static <R extends HttpRequest> R withApiKey(R request) {
if (request.getUrl().startsWith(PREFIX) && !apiKey.isEmpty()) {
request.header("X-API-KEY", apiKey);
}
return request;
}
public static boolean isAvailable() {
return !apiKey.isEmpty();
}
@@ -100,29 +109,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
}
@Override
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
int categoryId = 0;
if (category != null && category.getSelf() instanceof CurseAddon.Category) {
categoryId = ((CurseAddon.Category) category.getSelf()).getId();
}
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
pair("gameId", "432"),
pair("classId", Integer.toString(section)),
pair("categoryId", Integer.toString(categoryId)),
pair("gameVersion", gameVersion),
pair("searchFilter", searchFilter),
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
pair("sortOrder", toSortOrder(sortOrder)),
pair("index", Integer.toString(pageOffset * pageSize)),
pair("pageSize", Integer.toString(pageSize)))
.header("X-API-KEY", apiKey)
Response<List<CurseAddon>> response = withApiKey(HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", mapOf(
pair("gameId", "432"),
pair("classId", Integer.toString(section)),
pair("categoryId", Integer.toString(categoryId)),
pair("gameVersion", gameVersion),
pair("searchFilter", searchFilter),
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
pair("sortOrder", toSortOrder(sortOrder)),
pair("index", Integer.toString(pageOffset * pageSize)),
pair("pageSize", Integer.toString(pageSize)))))))
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
if (searchFilter.isEmpty()) {
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
}
// https://github.com/HMCL-dev/HMCL/issues/1549
String lowerCaseSearchFilter = searchFilter.toLowerCase();
String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT);
Map<String, Integer> searchFilterWords = new HashMap<>();
for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) {
searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1);
@@ -162,9 +170,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
Response<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints/432")
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
.header("X-API-KEY", apiKey)
.getJson(Response.typeOf(FingerprintMatchesResult.class));
if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) {
@@ -176,32 +183,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
@Override
public RemoteMod getModById(String id) throws IOException {
Response<CurseAddon> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id)
.header("X-API-KEY", apiKey)
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
.getJson(Response.typeOf(CurseAddon.class));
return response.data.toMod();
}
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
Response<CurseAddon.LatestFile> response = HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))
.header("X-API-KEY", apiKey)
Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId)))
.getJson(Response.typeOf(CurseAddon.LatestFile.class));
return response.getData().toVersion().getFile();
}
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
Response<List<CurseAddon.LatestFile>> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
pair("pageSize", "10000"))
.header("X-API-KEY", apiKey)
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
pair("pageSize", "10000")))
.getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class)));
return response.getData().stream().map(CurseAddon.LatestFile::toVersion);
}
public List<CurseAddon.Category> getCategoriesImpl() throws IOException {
Response<List<CurseAddon.Category>> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))
.header("X-API-KEY", apiKey)
Response<List<CurseAddon.Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
.getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class)));
return reorganizeCategories(categories.getData(), section);
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod.modrinth;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod;
@@ -79,7 +80,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
}
@Override
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
List<List<String>> facets = new ArrayList<>();
facets.add(Collections.singletonList("project_type:" + projectType));
if (StringUtils.isNotBlank(gameVersion)) {
@@ -95,7 +96,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
pair("limit", Integer.toString(pageSize)),
pair("index", convertSortType(sort))
);
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
Response<ProjectSearchResult> response = HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v2/search", query)))
.getJson(Response.typeOf(ProjectSearchResult.class));
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
@@ -120,7 +121,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
dependencies.add(builder.buildAsync());
}
Path rootPath = repository.getVersionRoot(version).toPath();
Path rootPath = repository.getVersionRoot(version).toPath().toAbsolutePath().normalize();
Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
.collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
Function.identity()));
@@ -128,12 +129,24 @@ public class ServerModpackCompletionTask extends Task<Void> {
Set<String> remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath)
.collect(Collectors.toSet());
Path runDirectory = repository.getRunDirectory(version).toPath().toAbsolutePath().normalize();
Path modsDirectory = runDirectory.resolve("mods");
int total = 0;
// for files in new modpack
for (ModpackConfiguration.FileInformation file : remoteManifest.getFiles()) {
Path actualPath = rootPath.resolve(file.getPath());
Path actualPath = rootPath.resolve(file.getPath()).toAbsolutePath().normalize();
String fileName = actualPath.getFileName().toString();
if (!actualPath.startsWith(rootPath)) {
throw new IOException("Unsecure path: " + file.getPath());
}
boolean download;
if (!files.containsKey(file.getPath())) {
if (!files.containsKey(file.getPath()) ||
modsDirectory.equals(actualPath.getParent())
&& Files.notExists(actualPath.resolveSibling(fileName + ModManager.DISABLED_EXTENSION))
&& Files.notExists(actualPath.resolveSibling(fileName + ModManager.OLD_EXTENSION))) {
// If old modpack does not have this entry, download it
download = true;
} else if (!Files.exists(actualPath)) {
@@ -38,11 +38,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class CacheFileTask extends FetchTask<Path> {
public CacheFileTask(@NotNull String uri) {
super(List.of(NetworkUtils.toURI(uri)));
this(NetworkUtils.toURI(uri));
}
public CacheFileTask(@NotNull URI uri) {
super(List.of(uri));
setName(uri.toString());
}
@Override
@@ -24,7 +24,6 @@ import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.ReflectionHelper;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
@@ -35,10 +34,9 @@ import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -1009,8 +1007,19 @@ public abstract class Task<T> {
void execute(T result, Exception exception) throws Exception;
}
private static final String PACKAGE_PREFIX = Task.class.getPackageName() + ".";
private static final Predicate<StackWalker.StackFrame> PREDICATE = stackFrame -> !stackFrame.getClassName().startsWith(PACKAGE_PREFIX);
private static final Function<Stream<StackWalker.StackFrame>, Optional<StackWalker.StackFrame>> FUNCTION = stream -> stream.filter(PREDICATE).findFirst();
private static final Function<StackWalker.StackFrame, String> FRAME_MAPPING = frame -> {
String fileName = frame.getFileName();
if (fileName != null)
return frame.getClassName() + '.' + frame.getMethodName() + '(' + fileName + ':' + frame.getLineNumber() + ')';
else
return frame.getClassName() + '.' + frame.getMethodName();
};
private static String getCaller() {
return ReflectionHelper.getCaller(packageName -> !"org.jackhuang.hmcl.task".equals(packageName)).toString();
return StackWalker.getInstance().walk(FUNCTION).map(FRAME_MAPPING).orElse("Unknown");
}
private static final class SimpleTask<T> extends Task<T> {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.util;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
@@ -75,7 +74,7 @@ public class CacheRepository {
index = new LinkedHashMap<>();
indexFileLastModified = null;
}
} catch (IOException | JsonParseException e) {
} catch (Exception e) {
LOG.warning("Unable to read index file", e);
index = new LinkedHashMap<>();
indexFileLastModified = null;
@@ -354,7 +353,7 @@ public class CacheRepository {
}
public int compareTo(ETagItem other) {
if (!url.equals(other.url))
if (!url.equals(other.url) && !NetworkUtils.toURI(url).equals(NetworkUtils.toURI(other.url)))
throw new IllegalArgumentException();
ZonedDateTime thisTime = Lang.ignoringException(() -> ZonedDateTime.parse(remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
@@ -1,122 +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;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.jar.JarOutputStream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class Pack200Utils {
private Pack200Utils() {
}
private static final String[] IMPL_NAMES = {
"java.util.jar.Pack200",
"org.glavo.pack200.Pack200",
"io.pack200.Pack200"
};
private static final MethodHandle newUnpackerHandle;
private static final MethodHandle unpackHandle;
private static final MethodHandle unpackFileHandle;
static {
Class<?> pack200Class = null;
Class<?> unpackerClass = null;
for (String implName : IMPL_NAMES) {
try {
pack200Class = Class.forName(implName);
unpackerClass = Class.forName(implName + "$Unpacker");
break;
} catch (ClassNotFoundException ignored) {
}
}
if (pack200Class == null) {
LOG.warning("Pack200 not found");
newUnpackerHandle = null;
unpackHandle = null;
unpackFileHandle = null;
} else {
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle newUnpacker = null;
MethodHandle unpack = null;
MethodHandle unpackFile = null;
try {
newUnpacker = lookup.findStatic(pack200Class, "newUnpacker", MethodType.methodType(unpackerClass));
unpack = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, InputStream.class, JarOutputStream.class));
unpackFile = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, File.class, JarOutputStream.class));
} catch (Throwable e) {
LOG.warning("Failed to find pack200 methods", e);
}
if (newUnpacker != null) {
newUnpackerHandle = newUnpacker;
unpackHandle = unpack;
unpackFileHandle = unpackFile;
} else {
newUnpackerHandle = null;
unpackHandle = null;
unpackFileHandle = null;
}
}
}
public static boolean isSupported() {
return newUnpackerHandle != null;
}
public static void unpack(InputStream in, JarOutputStream out) throws IOException {
if (newUnpackerHandle == null) {
throw new UnsupportedOperationException("Pack200");
}
try {
unpackHandle.invoke(newUnpackerHandle.invoke(), in, out);
} catch (IOException | RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public static void unpack(File in, JarOutputStream out) throws IOException {
if (newUnpackerHandle == null) {
throw new UnsupportedOperationException("Pack200");
}
try {
unpackFileHandle.invoke(newUnpackerHandle.invoke(), in, out);
} catch (IOException | RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
@@ -1,49 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.util.function.Predicate;
/**
*
* @author huangyuhui
*/
public final class ReflectionHelper {
private ReflectionHelper() {
}
/**
* Get caller, this method is caller sensitive.
*
* @param packageFilter returns false if we consider the given package is internal calls, not the caller
* @return the caller, method name, source file, line number
*/
public static StackTraceElement getCaller(Predicate<String> packageFilter) {
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
// element[0] is Thread.currentThread().getStackTrace()
// element[1] is ReflectionHelper.getCaller(packageFilter)
// so element[2] is caller of this method.
StackTraceElement caller = elements[2];
for (int i = 3; i < elements.length; ++i) {
if (packageFilter.test(StringUtils.substringBeforeLast(elements[i].getClassName(), '.')) &&
!caller.getClassName().equals(elements[i].getClassName()))
return elements[i];
}
return caller;
}
}
@@ -57,6 +57,10 @@ public abstract class HttpRequest {
this.method = method;
}
public String getUrl() {
return url;
}
public HttpRequest accept(String contentType) {
return header("Accept", contentType);
}
@@ -20,47 +20,51 @@ package org.jackhuang.hmcl.util.io;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.Properties;
public final class JarUtils {
private JarUtils() {
}
private static final Path THIS_JAR;
private static final Manifest manifest;
private static final Properties properties = new Properties();
static {
CodeSource cs = JarUtils.class.getProtectionDomain().getCodeSource();
Class<?> entryPointClass = null;
CodeSource cs = null;
try {
entryPointClass = Class.forName("org.jackhuang.hmcl.EntryPoint");
cs = entryPointClass.getProtectionDomain().getCodeSource();
} catch (ClassNotFoundException ignored) {
}
if (cs == null) {
THIS_JAR = null;
manifest = new Manifest();
} else {
Path path;
try {
path = Paths.get(cs.getLocation().toURI()).toAbsolutePath();
path = Path.of(cs.getLocation().toURI()).toAbsolutePath();
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
path = null;
}
if (path == null || !Files.isRegularFile(path)) {
THIS_JAR = null;
manifest = new Manifest();
} else {
THIS_JAR = path;
Manifest mn;
try (JarFile file = new JarFile(path.toFile())) {
mn = file.getManifest();
} catch (IOException e) {
mn = new Manifest();
THIS_JAR = path != null && Files.isRegularFile(path) ? path : null;
}
if (entryPointClass != null) {
InputStream input = entryPointClass.getResourceAsStream("/assets/hmcl.properties");
if (input != null) {
try (var reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
properties.load(reader);
} catch (IOException ignored) {
}
manifest = mn;
}
}
}
@@ -70,8 +74,7 @@ public final class JarUtils {
return THIS_JAR;
}
public static String getManifestAttribute(String name, String defaultValue) {
String value = manifest.getMainAttributes().getValue(name);
return value != null ? value : defaultValue;
public static String getAttribute(String name, String defaultValue) {
return properties.getProperty(name, defaultValue);
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util.io;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.io.*;
@@ -45,6 +46,19 @@ public final class NetworkUtils {
private NetworkUtils() {
}
public static boolean isLoopbackAddress(URI uri) {
String host = uri.getHost();
if (StringUtils.isBlank(host))
return false;
try {
InetAddress addr = InetAddress.getByName(host);
return addr.isLoopbackAddress();
} catch (UnknownHostException e) {
return false;
}
}
public static boolean isHttpUri(URI uri) {
return "http".equals(uri.getScheme()) || "https".equals(uri.getScheme());
}
@@ -146,7 +160,10 @@ public final class NetworkUtils {
boolean left = true;
while (i < location.length()) {
char ch = location.charAt(i);
if (ch == ' ' || ch >= 0x80)
if (ch == ' '
|| ch == '[' || ch == ']'
|| ch == '{' || ch == '}'
|| ch >= 0x80)
break;
else if (ch == '?')
left = false;
@@ -163,22 +180,6 @@ public final class NetworkUtils {
for (; i < location.length(); i++) {
char ch = location.charAt(i);
if (Character.isSurrogate(ch)) {
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
char ch2 = location.charAt(i + 1);
if (Character.isLowSurrogate(ch2)) {
int codePoint = Character.toCodePoint(ch, ch2);
encodeCodePoint(builder, codePoint);
i++;
continue;
}
}
// Invalid surrogate pair, encode as '?'
builder.append("%3F");
continue;
}
if (ch == ' ') {
if (left)
builder.append("%20");
@@ -187,7 +188,23 @@ public final class NetworkUtils {
} else if (ch == '?') {
left = false;
builder.append('?');
} else if (ch >= 0x80) {
} else if (ch >= 0x80 || (left && (ch == '[' || ch == ']' || ch == '{' || ch == '}'))) {
if (Character.isSurrogate(ch)) {
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
char ch2 = location.charAt(i + 1);
if (Character.isLowSurrogate(ch2)) {
int codePoint = Character.toCodePoint(ch, ch2);
encodeCodePoint(builder, codePoint);
i++;
continue;
}
}
// Invalid surrogate pair, encode as U+FFFD (replacement character)
encodeCodePoint(builder, 0xfffd);
continue;
}
encodeCodePoint(builder, ch);
} else {
builder.append(ch);
@@ -1,6 +1,24 @@
/*
* 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.logging;
import org.jackhuang.hmcl.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.tukaani.xz.LZMA2Options;
import org.tukaani.xz.XZOutputStream;
@@ -127,47 +145,10 @@ public final class Logger {
String caller = CLASS_NAME + ".onExit";
if (logRetention > 0 && logFile != null) {
List<Pair<Path, int[]>> list = new ArrayList<>();
Pattern fileNamePattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
Path dir = logFile.getParent();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path path : stream) {
Matcher matcher = fileNamePattern.matcher(path.getFileName().toString());
if (matcher.matches() && Files.isRegularFile(path)) {
int year = Integer.parseInt(matcher.group("year"));
int month = Integer.parseInt(matcher.group("month"));
int day = Integer.parseInt(matcher.group("day"));
int hour = Integer.parseInt(matcher.group("hour"));
int minute = Integer.parseInt(matcher.group("minute"));
int second = Integer.parseInt(matcher.group("second"));
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
list.add(Pair.pair(path, new int[]{year, month, day, hour, minute, second, n}));
}
}
} catch (IOException e) {
log(Level.WARNING, caller, "Failed to list log files in " + dir, e);
}
var list = findRecentLogFiles(Integer.MAX_VALUE);
if (list.size() > logRetention) {
list.sort((a, b) -> {
int[] v1 = a.getValue();
int[] v2 = b.getValue();
assert v1.length == v2.length;
for (int i = 0; i < v1.length; i++) {
int c = Integer.compare(v1[i], v2[i]);
if (c != 0)
return c;
}
return 0;
});
for (int i = 0, end = list.size() - logRetention; i < end; i++) {
Path file = list.get(i).getKey();
Path file = list.get(i);
try {
if (!Files.isSameFile(file, logFile)) {
log(Level.INFO, caller, "Delete old log file " + file, null);
@@ -276,6 +257,40 @@ public final class Logger {
return logFile;
}
public @NotNull List<Path> findRecentLogFiles(int n) {
if (n <= 0 || logFile == null)
return List.of();
var currentLogFile = LogFile.ofFile(logFile);
Path logDir = logFile.getParent();
if (logDir == null || !Files.isDirectory(logDir))
return List.of();
var logFiles = new ArrayList<LogFile>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(logDir)) {
for (Path path : stream) {
LogFile item = LogFile.ofFile(path);
if (item != null && (currentLogFile == null || item.compareTo(currentLogFile) < 0)) {
logFiles.add(item);
}
}
} catch (IOException e) {
log(Level.WARNING, CLASS_NAME + ".findRecentLogFiles", "Failed to list log files in " + logDir, e);
return List.of();
}
logFiles.sort(Comparator.naturalOrder());
final int resultLength = Math.min(n, logFiles.size());
final int offset = logFiles.size() - resultLength;
var result = new Path[resultLength];
for (int i = 0; i < resultLength; i++) {
result[i] = logFiles.get(i + offset).file;
}
return List.of(result);
}
public void exportLogs(OutputStream output) throws IOException {
Objects.requireNonNull(output);
LogEvent.ExportLog event = new LogEvent.ExportLog(output);
@@ -352,4 +367,69 @@ public final class Logger {
public void trace(String msg, Throwable exception) {
log(Level.TRACE, CallerFinder.getCaller(), msg, exception);
}
private static final class LogFile implements Comparable<LogFile> {
private static final Pattern FILE_NAME_PATTERN = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
private static @Nullable LogFile ofFile(Path file) {
if (!Files.isRegularFile(file))
return null;
Matcher matcher = FILE_NAME_PATTERN.matcher(file.getFileName().toString());
if (!matcher.matches())
return null;
int year = Integer.parseInt(matcher.group("year"));
int month = Integer.parseInt(matcher.group("month"));
int day = Integer.parseInt(matcher.group("day"));
int hour = Integer.parseInt(matcher.group("hour"));
int minute = Integer.parseInt(matcher.group("minute"));
int second = Integer.parseInt(matcher.group("second"));
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
return new LogFile(file, year, month, day, hour, minute, second, n);
}
private final Path file;
private final int year;
private final int month;
private final int day;
private final int hour;
private final int minute;
private final int second;
private final int n;
private LogFile(Path file, int year, int month, int day, int hour, int minute, int second, int n) {
this.file = file;
this.year = year;
this.month = month;
this.day = day;
this.hour = hour;
this.minute = minute;
this.second = second;
this.n = n;
}
@Override
public int compareTo(@NotNull Logger.LogFile that) {
if (this.year != that.year) return Integer.compare(this.year, that.year);
if (this.month != that.month) return Integer.compare(this.month, that.month);
if (this.day != that.day) return Integer.compare(this.day, that.day);
if (this.hour != that.hour) return Integer.compare(this.hour, that.hour);
if (this.minute != that.minute) return Integer.compare(this.minute, that.minute);
if (this.second != that.second) return Integer.compare(this.second, that.second);
if (this.n != that.n) return Integer.compare(this.n, that.n);
return 0;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day, hour, minute, second, n);
}
@Override
public boolean equals(Object obj) {
return obj instanceof LogFile && compareTo((LogFile) obj) == 0;
}
}
}
@@ -306,12 +306,39 @@ public enum OperatingSystem {
if (name.isEmpty())
return false;
// . and .. have special meaning on all platforms
if (name.equals("."))
return false;
// \0 and / are forbidden on all platforms
if (name.indexOf('/') != -1 || name.indexOf('\0') != -1)
if (name.equals(".") || name.equals(".."))
return false;
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
int codePoint;
if (Character.isSurrogate(ch)) {
if (!Character.isHighSurrogate(ch))
return false;
if (i == name.length() - 1)
return false;
char ch2 = name.charAt(++i);
if (!Character.isLowSurrogate(ch2))
return false;
codePoint = Character.toCodePoint(ch, ch2);
} else {
codePoint = ch;
}
if (!Character.isValidCodePoint(codePoint)
|| Character.isISOControl(codePoint)
|| codePoint == '/' || codePoint == '\0'
// Unicode replacement character
|| codePoint == 0xfffd
// Not Unicode character
|| codePoint == 0xfffe || codePoint == 0xffff)
return false;
}
if (CURRENT_OS == WINDOWS) { // Windows only
char lastChar = name.charAt(name.length() - 1);
// filenames ending in dot are not valid
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
@@ -93,6 +94,9 @@ final class WindowsGPUDetector {
return null;
} else if (object instanceof String[]) {
return String.join(" ", (String[]) object);
} else if (object instanceof byte[]) {
return new String((byte[]) object, StandardCharsets.UTF_16LE)
.replace("\0", "");
} else {
return object.toString();
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.util.versioning;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
@@ -98,8 +99,20 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
this.value = value;
}
public boolean isAprilFools() {
if (this instanceof Special)
return true;
if (this instanceof Snapshot) {
Snapshot snapshot = (Snapshot) this;
return snapshot.intValue == Snapshot.toInt(15, 14, 'a');
}
return false;
}
enum Type {
PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW
PRE_CLASSIC, CLASSIC, INDEV, INFDEV, ALPHA, BETA, NEW
}
abstract Type getType();
@@ -123,7 +136,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
return value;
}
static final class Old extends GameVersionNumber {
public static final class Old extends GameVersionNumber {
static Old parse(String value) {
Type type;
int prefixLength = 1;
@@ -137,11 +150,15 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
prefixLength = "rd-".length();
break;
case 'i':
if (!value.startsWith("inf-")) {
if (value.startsWith("inf-")) {
type = Type.INFDEV;
prefixLength = "inf-".length();
} else if (value.startsWith("in-")) {
type = Type.INDEV;
prefixLength = "in-".length();
} else {
throw new IllegalArgumentException(value);
}
type = Type.INFDEV;
prefixLength = "inf-".length();
break;
case 'a':
type = Type.ALPHA;
@@ -195,16 +212,16 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
}
}
static final class Release extends GameVersionNumber {
public static final class Release extends GameVersionNumber {
private static final Pattern PATTERN = Pattern.compile("1\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?((?<eaType>(-[a-zA-Z]+| Pre-Release ))(?<eaVersion>.+))?");
static final int TYPE_GA = Integer.MAX_VALUE;
public static final int TYPE_GA = Integer.MAX_VALUE;
static final int TYPE_UNKNOWN = 0;
static final int TYPE_EXP = 1;
static final int TYPE_PRE = 2;
static final int TYPE_RC = 3;
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_EXP = 1;
public static final int TYPE_PRE = 2;
public static final int TYPE_RC = 3;
static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, VersionNumber.ZERO);
@@ -243,6 +260,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
private final int minor;
private final int patch;
@MagicConstant(intValues = {TYPE_GA, TYPE_UNKNOWN, TYPE_EXP, TYPE_PRE, TYPE_RC})
private final int eaType;
private final VersionNumber eaVersion;
@@ -310,6 +328,26 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
throw new AssertionError(other.getClass());
}
public int getMajor() {
return major;
}
public int getMinor() {
return minor;
}
public int getPatch() {
return patch;
}
public int getEaType() {
return eaType;
}
public VersionNumber getEaVersion() {
return eaVersion;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, eaType, eaVersion);
@@ -324,7 +362,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
}
}
static final class Snapshot extends GameVersionNumber {
public static final class Snapshot extends GameVersionNumber {
static Snapshot parse(String value) {
if (value.length() != 6 || value.charAt(2) != 'w')
throw new IllegalArgumentException(value);
@@ -375,6 +413,18 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
throw new AssertionError(other.getClass());
}
public int getYear() {
return (intValue >> 16) & 0xff;
}
public int getWeek() {
return (intValue >> 8) & 0xff;
}
public char getSuffix() {
return (char) (intValue & 0xff);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -389,7 +439,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
}
}
static final class Special extends GameVersionNumber {
public static final class Special extends GameVersionNumber {
private VersionNumber versionNumber;
private GameVersionNumber prev;
@@ -18,17 +18,14 @@
package org.jackhuang.hmcl.util.versioning;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.Objects;
import java.util.*;
/**
* Copied from org.apache.maven.artifact.versioning.ComparableVersion
* Apache License 2.0
*
* <p>
* Maybe we can migrate to org.jenkins-ci:version-number:1.7?
*
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
*/
public final class VersionNumber implements Comparable<VersionNumber> {
@@ -220,9 +217,17 @@ public final class VersionNumber implements Comparable<VersionNumber> {
*/
private static final class StringItem implements Item {
private final String value;
private final boolean pre;
StringItem(String value) {
this.value = value;
String lower = value.trim().toLowerCase(Locale.ROOT);
this.pre = lower.startsWith("alpha")
|| lower.startsWith("beta")
|| lower.startsWith("pre")
|| lower.startsWith("rc")
|| lower.startsWith("experimental");
}
public int getType() {
@@ -235,8 +240,8 @@ public final class VersionNumber implements Comparable<VersionNumber> {
public int compareTo(Item item) {
if (item == null) {
// 1-string > 1
return 1;
// 1-beta < 1 < 1-string
return pre ? -1 : 1;
}
switch (item.getType()) {
case LONG_ITEM:
@@ -1,4 +1,4 @@
# https://gitlab.freedesktop.org/mesa/drm/-/blob/b65d6ede3ebe64dbde5805007ddef22f390f8050/data/amdgpu.ids
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/7d43d9b6fe5df4b234937fce9e51adb1b327ded1/data/amdgpu.ids
#
# List of AMDGPU IDs
#
@@ -591,7 +591,9 @@
74B6, 00, AMD Instinct MI308X
74BD, 00, AMD Instinct MI300X HF
7550, C0, AMD Radeon RX 9070 XT
7550, C2, AMD Radeon RX 9070 GRE
7550, C3, AMD Radeon RX 9070
7551, C0, AMD Radeon AI PRO R9700
9830, 00, AMD Radeon HD 8400 / R3 Series
9831, 00, AMD Radeon HD 8400E
9832, 00, AMD Radeon HD 8330
@@ -19,33 +19,50 @@ package org.jackhuang.hmcl.util.io;
import org.junit.jupiter.api.Test;
import java.net.URI;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.io.NetworkUtils.encodeLocation;
import static org.jackhuang.hmcl.util.io.NetworkUtils.getCharsetFromContentType;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.jackhuang.hmcl.util.io.NetworkUtils.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Glavo
*/
public class NetworkUtilsTest {
@Test
public void testIsLoopbackAddress() {
assertTrue(isLoopbackAddress(URI.create("https://127.0.0.1/test")));
assertTrue(isLoopbackAddress(URI.create("https://127.0.0.1:8080/test")));
assertTrue(isLoopbackAddress(URI.create("https://localhost/test")));
assertTrue(isLoopbackAddress(URI.create("https://localhost:8080/test")));
assertTrue(isLoopbackAddress(URI.create("https://[::1]/test")));
assertTrue(isLoopbackAddress(URI.create("https://[::1]:8080/test")));
assertFalse(isLoopbackAddress(URI.create("https://www.example.com/test")));
assertFalse(isLoopbackAddress(URI.create("https://www.example.com:8080/test")));
}
@Test
public void testEncodeLocation() {
assertEquals("https://github.com", encodeLocation("https://github.com"));
assertEquals("https://github.com/HMCL-dev/HMCL/commits?author=Glavo", encodeLocation("https://github.com/HMCL-dev/HMCL/commits?author=Glavo"));
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file with space"));
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file%20with%20space"));
assertEquals("https://www.example.com/%5Bfile%5D", encodeLocation("https://www.example.com/[file]"));
assertEquals("https://www.example.com/%7Bfile%7D", encodeLocation("https://www.example.com/{file}"));
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95", encodeLocation("https://www.example.com/测试"));
assertEquals("https://www.example.com/%F0%9F%98%87", encodeLocation("https://www.example.com/\uD83D\uDE07"));
assertEquals("https://www.example.com/test?a=10+20", encodeLocation("https://www.example.com/test?a=10 20"));
assertEquals("https://www.example.com/test?a=10+20&b=[30]&c={40}", encodeLocation("https://www.example.com/test?a=10 20&b=[30]&c={40}"));
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95?a=10+20", encodeLocation("https://www.example.com/测试?a=10 20"));
// Invalid surrogate pair
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uD83D"));
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uDE07"));
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uD83Dtest"));
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uDE07test"));
assertEquals("https://www.example.com/%EF%BF%BD", encodeLocation("https://www.example.com/\uD83D"));
assertEquals("https://www.example.com/%EF%BF%BD", encodeLocation("https://www.example.com/\uDE07"));
assertEquals("https://www.example.com/%EF%BF%BDtest", encodeLocation("https://www.example.com/\uD83Dtest"));
assertEquals("https://www.example.com/%EF%BF%BDtest", encodeLocation("https://www.example.com/\uDE07test"));
}
@Test
@@ -0,0 +1,55 @@
/*
* 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.platform;
import org.junit.jupiter.api.Test;
import static org.jackhuang.hmcl.util.platform.OperatingSystem.isNameValid;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author Glavo
*/
public final class OperatingSystemTest {
@Test
public void testIsNameValid() {
assertTrue(isNameValid("example"));
assertTrue(isNameValid("example.zip"));
assertTrue(isNameValid("example.tar.gz"));
assertTrue(isNameValid("\uD83D\uDE00"));
assertTrue(isNameValid("a\uD83D\uDE00b"));
assertFalse(isNameValid("."));
assertFalse(isNameValid(".."));
assertFalse(isNameValid("exam\0ple"));
assertFalse(isNameValid("example/0"));
// Test for invalid surrogate pair
assertFalse(isNameValid("\uD83D"));
assertFalse(isNameValid("\uDE00"));
assertFalse(isNameValid("\uDE00\uD83D"));
assertFalse(isNameValid("\uD83D\uD83D"));
assertFalse(isNameValid("a\uD83D"));
assertFalse(isNameValid("a\uDE00"));
assertFalse(isNameValid("a\uDE00\uD83D"));
assertFalse(isNameValid("a\uD83Db"));
assertFalse(isNameValid("a\uDE00b"));
assertFalse(isNameValid("a\uDE00\uD83Db"));
}
}
@@ -99,9 +99,34 @@ public final class GameVersionNumberTest {
assertEquals(VersionNumber.asVersion(versionNumber), old.versionNumber);
}
private static boolean isAprilFools(String version) {
return GameVersionNumber.asGameVersion(version).isAprilFools();
}
@Test
public void testIsAprilFools() {
assertTrue(isAprilFools("15w14a"));
assertTrue(isAprilFools("1.RV-Pre1"));
assertTrue(isAprilFools("3D Shareware v1.34"));
assertTrue(isAprilFools("2.0"));
assertTrue(isAprilFools("20w14infinite"));
assertTrue(isAprilFools("22w13oneBlockAtATime"));
assertTrue(isAprilFools("23w13a_or_b"));
assertTrue(isAprilFools("24w14potato"));
assertTrue(isAprilFools("25w14craftmine"));
assertFalse(isAprilFools("1.21.8"));
assertFalse(isAprilFools("1.21.8-rc1"));
assertFalse(isAprilFools("25w21a"));
assertFalse(isAprilFools("13w12~"));
assertFalse(isAprilFools("15w14b"));
}
@Test
public void testParseOld() {
assertOldVersion("rd-132211", GameVersionNumber.Type.PRE_CLASSIC, "132211");
assertOldVersion("in-20100223", GameVersionNumber.Type.INDEV, "20100223");
assertOldVersion("in-20100212-2", GameVersionNumber.Type.INDEV, "20100212-2");
assertOldVersion("inf-20100618", GameVersionNumber.Type.INFDEV, "20100618");
assertOldVersion("inf-20100330-1", GameVersionNumber.Type.INFDEV, "20100330-1");
assertOldVersion("a1.0.6", GameVersionNumber.Type.ALPHA, "1.0.6");
@@ -90,12 +90,17 @@ public class VersionNumberTest {
assertLessThan("1.99999999999999999999", "1.199999999999999999999");
assertLessThan("1.99999999999999999999", "2");
assertLessThan("1.99999999999999999999", "2.0");
assertLessThan("1.0", "1.0-zzz");
assertLessThan("1.0-beta.1", "1.0");
assertLessThan("1.0-alpha.1", "1.0-beta.1");
assertLessThan("3.6.15", "3.6.15.289");
assertLessThan("3.6.15.289", "3.6.16");
}
@Test
public void testSorting() {
final Comparator<String> comparator = ((Comparator<String>) VersionNumber::compare).thenComparing(String::compareTo);
final List<String> input = Collections.unmodifiableList(Arrays.asList(
final List<String> input = List.of(
"0",
"0.10.0",
"1.6.4",
@@ -120,6 +125,9 @@ public class VersionNumberTest {
"1.8.9",
"1.8.9-forge1902",
"1.9",
"1.10-alpha.2",
"1.10-beta.1",
"1.10-beta.2",
"1.10",
"1.10.2",
"1.10.2-AOE",
@@ -140,7 +148,8 @@ public class VersionNumberTest {
"1.99999999999999999999",
"2",
"2.0",
"2.1"));
"2.1"
);
List<String> output = new ArrayList<>(input);
output.sort(comparator);
+4 -4
View File
@@ -4,13 +4,13 @@
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.7) |
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (Old World) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
+4 -4
View File
@@ -4,13 +4,13 @@
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.7) |
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
+4 -4
View File
@@ -4,13 +4,13 @@
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.7) |
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
+1 -1
View File
@@ -52,7 +52,7 @@ Simply execute the following command in the project root directory:
./gradlew clean build
```
Make sure you have Java 11 or later installed.
Make sure you have Java 17 or later installed.
## JVM Options (for debugging)
+1 -1
View File
@@ -52,7 +52,7 @@ HMCL 有着强大的跨平台能力。它不仅支持 Windows、Linux、macOS
./gradlew clean build
```
请确保你至少安装了 JDK 11 或更高版本。
请确保你至少安装了 JDK 17 或更高版本。
## JVM 选项 (用于调试)
+1 -1
View File
@@ -52,7 +52,7 @@ HMCL 有著強大的跨平臺能力。它不僅支援 Windows、Linux、macOS
./gradlew clean build
```
請確保你至少安裝了 JDK 11 或更高版本。
請確保你至少安裝了 JDK 17 或更高版本。
## JVM 選項 (用於除錯)
+3 -1
View File
@@ -16,12 +16,13 @@ twelvemonkeys = "3.12.0"
jna = "5.17.0"
pci-ids = "0.4.0"
java-info = "1.0"
authlib-injector = "1.2.5"
# testing
junit = "5.13.4"
# plugins
shadow = "8.3.6"
shadow = "9.0.1"
[libraries]
hmclauncher = { module = "org.glavo.hmcl:HMCLauncher", version.ref = "hmclauncher" }
@@ -43,6 +44,7 @@ jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }
java-info = { module = "org.glavo:java-info", version.ref = "java-info" }
authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" }
# testing
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }