Compare commits

..
Author SHA1 Message Date
Glavo 88637b33bc 更新 GROUPS_URL (#4004) 2025-06-15 09:34:24 +08:00
Burning_TNT 462eea3539 Fix #3997: 模组搜索返回结果为空时下载界面异常 (#3998) 2025-06-14 21:23:39 +08:00
Burning_TNT d624086594 解析 MultiMC 整合包中未声明但在依赖链上的组件 (#4003) 2025-06-14 21:23:21 +08:00
Burning_TNT fa8b3a952b Fix #3999: 自动安装更改 MC 版本后无法点击安装键 (#4001) 2025-06-14 21:12:59 +08:00
Burning_TNT c70a6c3372 移除漏洞反馈 Issue 模板中的系统信息 (#4000) 2025-06-14 21:12:33 +08:00
Glavo 0616c37de7 创建世界管理页面 (#3991) 2025-06-13 14:28:51 +08:00
Burning_TNT b72056f7fe Fix #3994: 修复 Controller 未能读取 JAVA_VERSION_TIP 的问题 (#3995) 2025-06-12 20:49:55 +08:00
Glavo 3ebe8dce44 修复在 schematics 文件夹不存在的情况下,打开投影管理页面会抛出异常的问题 (#3996) 2025-06-12 20:46:04 +08:00
Glavo a9c28573b8 Bump HMCLauncher to 3.6.0.4 (#3992) 2025-06-12 12:38:45 +08:00
9bead7e34e 支持下载光影 (#2794)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes. Provide support for downloading shaderpacks from Modrinth.

* Fix.

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

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

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

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

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

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

* Update Icon

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-06-11 19:30:47 +08:00
3gf8jv4dvandGlavo d0b36ffa15 fix(i18n): localize MiB in VersionSettingsPage (#3984)
For Slavic languages (Russian, Ukrainian, etc.) they need translations.

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 01:43:05 +08:00
辞庐andGlavo ea081333e7 自动安装页面隐藏 mcbbs 项 (#3836)
* update

* CheckStyle

* Update InstallerListPage.java

CheckStyle

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 01:25:59 +08:00
3gf8jv4dv 2b99da430f chore(locale): add folder.schematics to spanish and russian (#3983) 2025-06-11 01:16:20 +08:00
Wulian233andGlavo f1ea316bad feat: 更新日志中仅显示本次更新内容 (#3974)
* 更新日志中仅显示本次更新内容

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 00:57:30 +08:00
3gf8jv4dv 9565c3bdfe fix(locale): fix units of information in russian (#3985) 2025-06-11 00:32:28 +08:00
3gf8jv4dv 710fd0f075 fix(locale): fix units of information in japanese (#3986) 2025-06-11 00:29:23 +08:00
33 changed files with 715 additions and 300 deletions

No files matched your search

+1 -11
View File
@@ -20,16 +20,6 @@ body:
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
- type: input
id: platform
attributes:
label: 平台 | Platform
description: |
请输入你遇到问题的平台。
Please enter the platform on which you encountered the bug.
placeholder: e.g. Windows 11
validations:
required: true
- type: textarea
id: bug-report
attributes:
@@ -56,4 +46,4 @@ body:
If your launcher does not crash, please DO NOT EXIT your launcher, click "Export Launcher Logs" in the "Settings → General → Debug" of the launcher, and drag the exported log to the following input field.
**ATTENTION: The crash report or log file is the key to resolving the bug. Please upload them!**
validations:
required: true
required: true
+1 -1
View File
@@ -44,7 +44,7 @@ dependencies {
implementation(libs.java.info)
if (launcherExe == null) {
implementation("org.glavo.hmcl:HMCLauncher:3.6.0.3")
implementation("org.glavo.hmcl:HMCLauncher:3.6.0.4")
}
}
@@ -47,7 +47,7 @@ public final class Metadata {
public static final String CONTACT_URL = DOCS_URL + "/help.html";
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = DOCS_URL + "/groups.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);
@@ -312,15 +312,15 @@ public final class Controllers {
}
if (JavaRuntime.CURRENT_VERSION < 10) {
Integer shownTipVersion = null;
Number shownTipVersion = null;
try {
shownTipVersion = (Integer) config().getShownTips().get(JAVA_VERSION_TIP);
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 < 11) {
if (shownTipVersion == null || shownTipVersion.intValue() < 11) {
String downloadLink = null;
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
@@ -58,12 +58,14 @@ public enum SVG {
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
@@ -73,9 +75,11 @@ public enum SVG {
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE("M6 21V19H18V21H6Z"),
@@ -104,6 +108,8 @@ public enum SVG {
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
;
@@ -127,11 +133,10 @@ public enum SVG {
return pane;
}
Group svg = new Group(path);
double scale = size / 24;
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
public Node createIcon(ObservableValue<? extends Paint> fill, double size) {
@@ -51,7 +51,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
if (node == null)
if (node == null || !"h1".equals(node.nodeName()))
throw new IOException("Cannot find #nowchange in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
@@ -60,6 +60,9 @@ public final class UpgradeDialog extends JFXDialogLayout {
});
do {
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
break;
}
renderer.appendNode(node);
node = node.nextSibling();
} while (node != null);
@@ -311,8 +311,9 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
// FIXME: Get WorldPage working first, and revisit this later
page.closePage();
if (page.isPageCloseable()) {
page.closePage();
return;
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.download;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.download.DownloadProvider;
@@ -58,6 +59,8 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
reload();
});
}
installable.bind(Bindings.createBooleanBinding(() -> compatible.get() && txtName.validate(), txtName.textProperty(), compatible));
}
@Override
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -72,6 +73,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<DownloadListPage> modTab = new TabHeader.Tab<>("modTab");
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<DownloadListPage> shaderTab = new TabHeader.Tab<>("shaderTab");
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
private final TransitionPane transitionPane = new TransitionPane();
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
@@ -98,8 +100,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}));
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, (profile, version, file) -> download(profile, version, file, "shaderpacks"), true)));
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
Profiles.registerVersionsListener(this::loadVersions);
@@ -115,6 +118,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
.startCategory(i18n("download.content").toUpperCase(Locale.ROOT))
.addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION)
.addNavigationDrawerTab(tab, resourcePackTab, i18n("resourcepack"), SVG.TEXTURE)
.addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY)
.addNavigationDrawerTab(tab, worldTab, i18n("world"), SVG.PUBLIC);
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
@@ -186,6 +190,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
}
if (shaderTab.isInitialized()) {
shaderTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
@@ -28,7 +26,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -41,24 +38,17 @@ import java.util.Objects;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
private final Path worldDir;
private final Datapack datapack;
// Strongly referencing items, preventing GC collection
@SuppressWarnings("FieldCanBeLocal")
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir;
state.set(State.fromTitle(i18n("datapack.title", worldName)));
public DatapackListPage(WorldManagePage worldManagePage) {
this.worldDir = worldManagePage.getWorld().getFile();
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
setItems(items = MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
setItems(MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
@@ -91,11 +81,6 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
.start();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.geometry.Insets;
@@ -29,87 +28,57 @@ import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class DatapackListPageSkin extends SkinBase<DatapackListPage> {
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
DatapackListPageSkin(DatapackListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
root.getStyleClass().add("content-background");
StackPane pane = new StackPane();
pane.setPadding(new Insets(10));
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
JFXListView<DatapackInfoObject> listView = new JFXListView<>();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG.ADD, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("button.remove"), SVG.DELETE, () -> {
toolbar.getChildren().add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add));
toolbar.getChildren().add(createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
}, null);
}));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG.CHECK, () ->
toolbar.getChildren().add(createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG.CLOSE, () ->
toolbar.getChildren().add(createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
root.getContent().add(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new FloatListCell<DatapackInfoObject>(listView) {
JFXCheckBox checkBox = new JFXCheckBox();
TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
HBox container = new HBox(8);
container.setPadding(new Insets(0, 0, 0, 6));
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
pane.setPadding(new Insets(8, 8, 8, 0));
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(checkBox, content);
}
@Override
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
}
});
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new DatapackInfoListCell(listView, lastCell));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
@@ -117,10 +86,11 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
center.setContent(listView);
root.setCenter(center);
root.getContent().add(center);
}
getChildren().setAll(root);
pane.getChildren().setAll(root);
getChildren().setAll(pane);
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
@@ -144,4 +114,36 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
return packInfo;
}
}
private static final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, Holder<Object> lastCell) {
super(listView, lastCell);
HBox container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(content, Priority.ALWAYS);
content.setMouseTransparent(true);
setSelectable();
StackPane.setMargin(container, new Insets(8));
container.getChildren().setAll(checkBox, content);
getContainer().getChildren().setAll(container);
}
@Override
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
}
}
}
@@ -356,11 +356,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
currentFilterID.set(filterID.get());
int pageOffset = control.pageOffset.get();
getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(),
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
.map(CategoryIndented::getCategory)
.orElse(null),
control.pageOffset.get(),
pageOffset == -1 ? 0 : pageOffset,
nameField.getText(),
sortComboBox.getSelectionModel().getSelectedItem());
};
@@ -432,13 +433,15 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
int pageOffset = control.pageOffset.get();
int pageCount = control.pageCount.get();
boolean disablePrevious = pageOffset == 0;
boolean disableAll = pageCount >= -1 && pageCount <= 1;
boolean disablePrevious = disableAll || pageOffset == 0;
firstPageButton.setDisable(disablePrevious);
previousPageButton.setDisable(disablePrevious);
boolean disableNext = pageOffset == pageCount - 1;
boolean disableNext = disableAll || pageOffset == pageCount - 1;
nextPageButton.setDisable(disableNext);
lastPageButton.setDisable(disableNext || pageCount == -1);
lastPageButton.setDisable(disableNext);
};
FXUtils.onChange(control.pageCount, pageCountN -> {
@@ -114,6 +114,8 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
if ("mcbbs".equals(libraryId))
continue;
// we have done this library above.
if (LibraryAnalyzer.LibraryType.fromPatchId(libraryId) != null)
@@ -50,6 +50,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
@@ -206,6 +207,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
}
}
}
} catch (NoSuchFileException ignored) {
} catch (IOException e) {
LOG.warning("Failed to load schematics in " + dir, e);
}
@@ -315,7 +315,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
txtMaxMemory.textProperty().bindBidirectional(maxMemory, SafeStringConverter.fromInteger());
txtMaxMemory.setValidators(new NumberValidator(i18n("input.number"), false));
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label("MiB"));
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label(i18n("settings.memory.unit.mib")));
}
StackPane progressBarPane = new StackPane();
@@ -37,15 +37,17 @@ public final class WorldBackupTask extends Task<Path> {
private final World world;
private final Path backupsDir;
private final boolean needLock;
public WorldBackupTask(World world, Path backupsDir) {
public WorldBackupTask(World world, Path backupsDir, boolean needLock) {
this.world = world;
this.backupsDir = backupsDir;
this.needLock = needLock;
}
@Override
public void execute() throws Exception {
try (FileChannel lockChannel = world.lock()) {
try (FileChannel lockChannel = needLock ? world.lock() : null) {
Files.createDirectories(backupsDir);
String time = LocalDateTime.now().format(WorldBackupsPage.TIME_FORMATTER);
String baseName = time + "_" + world.getFileName();
@@ -18,9 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -41,7 +38,6 @@ import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
@@ -65,33 +61,24 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> implements DecoratorPage {
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> {
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
private final World world;
private final Path backupsDir;
private final boolean isReadOnly;
private final Pattern backupFileNamePattern;
private final ObjectProperty<State> state = new SimpleObjectProperty<>();
public WorldBackupsPage(World world, Path backupsDir) {
this.backupsDir = backupsDir;
this.world = world;
public WorldBackupsPage(WorldManagePage worldManagePage) {
this.world = worldManagePage.getWorld();
this.backupsDir = worldManagePage.getBackupsDir();
this.isReadOnly = worldManagePage.isReadOnly();
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
this.state.set(State.fromTitle(i18n("world.backup.title", world.getWorldName())));
loadBackups();
refresh();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
@Override
public void refresh() {
loadBackups();
}
private void loadBackups() {
setLoading(true);
Task.supplyAsync(() -> {
if (Files.isDirectory(backupsDir)) {
@@ -140,7 +127,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
}
void createBackup() {
Controllers.taskDialog(new WorldBackupTask(world, backupsDir).setName(i18n("world.backup.processing")).thenApplyAsync(path -> {
Controllers.taskDialog(new WorldBackupTask(world, backupsDir, false).setName(i18n("world.backup.processing")).thenApplyAsync(path -> {
Matcher matcher = backupFileNamePattern.matcher(path.getFileName().toString());
if (!matcher.matches()) {
throw new AssertionError("Wrong backup file name" + path);
@@ -176,7 +163,13 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
@Override
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
return Arrays.asList(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup));
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
createBackup.setDisable(isReadOnly);
return Arrays.asList(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createBackup
);
}
}
@@ -20,25 +20,27 @@ package org.jackhuang.hmcl.ui.versions;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.time.Instant;
@@ -52,38 +54,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
public final class WorldInfoPage extends SpinnerPane {
private final WorldManagePage worldManagePage;
private final World world;
private CompoundTag levelDat;
private final ObjectProperty<State> stateProperty;
private FileChannel sessionLockChannel;
@Override
public boolean back() {
closePage();
return true;
}
@Override
public void closePage() {
if (sessionLockChannel != null) {
try {
sessionLockChannel.close();
} catch (IOException e) {
LOG.warning("Failed to close session lock channel", e);
}
sessionLockChannel = null;
}
}
public WorldInfoPage(World world) {
this.world = world;
this.stateProperty = new SimpleObjectProperty<>(State.fromTitle(i18n("world.info.title", world.getWorldName())));
this.getStyleClass().add("gray-background");
public WorldInfoPage(WorldManagePage worldManagePage) {
this.worldManagePage = worldManagePage;
this.world = worldManagePage.getWorld();
this.setLoading(true);
Task.supplyAsync(this::loadWorldInfo)
@@ -103,18 +81,9 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
if (!Files.isDirectory(world.getFile()))
throw new IOException("Not a valid world directory");
try {
sessionLockChannel = world.lock();
} catch (IOException ignored) {
}
return world.readLevelDat();
}
private boolean isReadOnly() {
return sessionLockChannel == null;
}
private void updateControls() {
CompoundTag dataTag = levelDat.get("Data");
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
@@ -161,9 +130,22 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
BorderPane randomSeedPane = new BorderPane();
{
HBox left = new HBox(8);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setAlignment(Pos.CENTER_LEFT);
randomSeedPane.setLeft(left);
Label label = new Label(i18n("world.info.random_seed"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
randomSeedPane.setLeft(label);
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
StackPane visibilityButton = new StackPane();
visibilityButton.setCursor(Cursor.HAND);
FXUtils.setLimitWidth(visibilityButton, 12);
FXUtils.setLimitHeight(visibilityButton, 12);
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
left.getChildren().setAll(label, visibilityButton);
Label randomSeedLabel = new Label();
FXUtils.copyOnDoubleClick(randomSeedLabel);
@@ -174,6 +156,14 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
if (tag instanceof LongTag) {
randomSeedLabel.setText(tag.getValue().toString());
}
BoxBlur blur = new BoxBlur();
blur.setIterations(3);
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
visibilityButton.getChildren().setAll(icon.createIcon(Theme.blackFill(), 12));
randomSeedLabel.setEffect(isVisibility ? null : blur);
});
}
BorderPane lastPlayedPane = new BorderPane();
@@ -210,7 +200,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
OptionToggleButton allowCheatsButton = new OptionToggleButton();
{
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
allowCheatsButton.setDisable(isReadOnly());
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
Tag tag = dataTag.get("allowCommands");
if (tag instanceof ByteTag) {
@@ -233,7 +223,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
{
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
generateFeaturesButton.setDisable(isReadOnly());
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
if (tag instanceof ByteTag) {
@@ -260,7 +250,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
difficultyPane.setLeft(label);
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
difficultyBox.setDisable(isReadOnly());
difficultyBox.setDisable(worldManagePage.isReadOnly());
BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT);
difficultyPane.setRight(difficultyBox);
@@ -366,7 +356,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
playerGameTypePane.setLeft(label);
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
gameTypeBox.setDisable(isReadOnly());
gameTypeBox.setDisable(worldManagePage.isReadOnly());
BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT);
playerGameTypePane.setRight(gameTypeBox);
@@ -397,7 +387,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
healthPane.setLeft(label);
JFXTextField healthField = new JFXTextField();
healthField.setDisable(isReadOnly());
healthField.setDisable(worldManagePage.isReadOnly());
healthField.setPrefWidth(50);
healthField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(healthField, Pos.CENTER_RIGHT);
@@ -431,7 +421,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
foodLevelPane.setLeft(label);
JFXTextField foodLevelField = new JFXTextField();
foodLevelField.setDisable(isReadOnly());
foodLevelField.setDisable(worldManagePage.isReadOnly());
foodLevelField.setPrefWidth(50);
foodLevelField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(foodLevelField, Pos.CENTER_RIGHT);
@@ -465,7 +455,7 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
xpLevelPane.setLeft(label);
JFXTextField xpLevelField = new JFXTextField();
xpLevelField.setDisable(isReadOnly());
xpLevelField.setDisable(worldManagePage.isReadOnly());
xpLevelField.setPrefWidth(50);
xpLevelField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(xpLevelField, Pos.CENTER_RIGHT);
@@ -510,11 +500,6 @@ public final class WorldInfoPage extends SpinnerPane implements DecoratorPage {
}
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return stateProperty;
}
private static final class Dimension {
static final Dimension OVERWORLD = new Dimension(null);
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.nio.file.Path;
@@ -66,20 +65,7 @@ public final class WorldListItem extends Control {
FXUtils.openFolder(world.getFile().toFile());
}
public void manageDatapacks() {
if (world.getGameVersion() == null || // old game will not write game version to level.dat
GameVersionNumber.compare(world.getGameVersion(), "1.13") < 0) {
Controllers.dialog(i18n("world.datapack.1_13"));
return;
}
Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile()));
}
public void showInfo() {
Controllers.navigate(new WorldInfoPage(world));
}
public void showBackupPage() {
Controllers.navigate(new WorldBackupsPage(world, backupsDir));
public void showManagePage() {
Controllers.navigate(new WorldManagePage(world, backupsDir));
}
}
@@ -18,10 +18,12 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -29,8 +31,9 @@ import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.time.Instant;
@@ -40,12 +43,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class WorldListItemSkin extends SkinBase<WorldListItem> {
private final BorderPane root;
public WorldListItemSkin(WorldListItem skinnable) {
super(skinnable);
World world = skinnable.getWorld();
BorderPane root = new BorderPane();
root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
@@ -64,6 +69,7 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
{
TwoLineListItem item = new TwoLineListItem();
root.setCenter(item);
item.setMouseTransparent(true);
if (world.getWorldName() != null)
item.setTitle(parseColorEscapes(world.getWorldName()));
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
@@ -79,42 +85,58 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = new JFXButton();
right.getChildren().add(btnReveal);
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), -1));
btnReveal.setOnAction(event -> skinnable.reveal());
JFXButton btnExport = new JFXButton();
right.getChildren().add(btnExport);
FXUtils.installFastTooltip(btnExport, i18n("world.export"));
btnExport.getStyleClass().add("toggle-icon4");
btnExport.setGraphic(SVG.OUTPUT.createIcon(Theme.blackFill(), -1));
btnExport.setOnAction(event -> skinnable.export());
JFXButton btnBackup = new JFXButton();
right.getChildren().add(btnBackup);
FXUtils.installFastTooltip(btnBackup, i18n("world.backup"));
btnBackup.getStyleClass().add("toggle-icon4");
btnBackup.setGraphic(SVG.ARCHIVE.createIcon(Theme.blackFill(), -1));
btnBackup.setOnAction(event -> skinnable.showBackupPage());
JFXButton btnDatapack = new JFXButton();
right.getChildren().add(btnDatapack);
FXUtils.installFastTooltip(btnDatapack, i18n("world.datapack"));
btnDatapack.getStyleClass().add("toggle-icon4");
btnDatapack.setGraphic(SVG.EXTENSION.createIcon(Theme.blackFill(), -1));
btnDatapack.setOnAction(event -> skinnable.manageDatapacks());
JFXButton btnInfo = new JFXButton();
right.getChildren().add(btnInfo);
FXUtils.installFastTooltip(btnInfo, i18n("world.info"));
btnInfo.getStyleClass().add("toggle-icon4");
btnInfo.setGraphic(SVG.INFO.createIcon(Theme.blackFill(), -1));
btnInfo.setOnAction(event -> skinnable.showInfo());
JFXButton btnMore = new JFXButton();
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
btnMore.setGraphic(SVG.MORE_VERT.createIcon(Theme.blackFill(), -1));
btnMore.setOnAction(event -> showPopupMenu(JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()));
}
getChildren().setAll(new RipplerContainer(root));
RipplerContainer container = new RipplerContainer(root);
container.setOnMouseClicked(event -> {
if (event.getClickCount() != 1)
return;
if (event.getButton() == MouseButton.PRIMARY)
skinnable.showManagePage();
else if (event.getButton() == MouseButton.SECONDARY)
showPopupMenu(JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
});
getChildren().setAll(container);
}
// Popup Menu
public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
WorldListItem item = getSkinnable();
World world = item.getWorld();
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
if (ChunkBaseApp.isSupported(world)) {
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
() -> ChunkBaseApp.openEndCityFinder(world), popup));
}
}
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup));
popup.show(root, JFXPopup.PopupVPosition.TOP, hPosition, initOffsetX, initOffsetY);
}
}
@@ -0,0 +1,168 @@
/*
* 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.ui.versions;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World;
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.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
private final ObjectProperty<State> state;
private final World world;
private final Path backupsDir;
private final TabHeader header;
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
private final TransitionPane transitionPane = new TransitionPane();
private FileChannel sessionLockChannel;
public WorldManagePage(World world, Path backupsDir) {
this.world = world;
this.backupsDir = backupsDir;
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", world.getWorldName())));
this.header = new TabHeader(worldInfoTab, worldBackupsTab);
if (world.getGameVersion() != null && // old game will not write game version to level.dat
GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
header.getTabs().add(datapackTab);
}
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
header.select(worldInfoTab);
transitionPane.setContent(worldInfoTab.getNode(), ContainerAnimations.NONE);
FXUtils.onChange(header.getSelectionModel().selectedItemProperty(), newValue ->
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE));
setCenter(transitionPane);
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
VBox.setVgrow(left, Priority.ALWAYS);
setLeft(left);
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO)
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE)
.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION);
left.setTop(sideBar);
AdvancedListBox toolbar = new AdvancedListBox();
if (ChunkBaseApp.isSupported(world)) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
popupMenu.getContent().add(
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
}
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
chunkBaseMenuItem.setOnAction(e ->
popup.show(chunkBaseMenuItem,
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
chunkBaseMenuItem.getWidth(), 0)));
}
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile().toFile()), null);
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
left.setBottom(toolbar);
// Does it need to be done in the background?
try {
sessionLockChannel = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
} catch (IOException ignored) {
}
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
public World getWorld() {
return world;
}
public Path getBackupsDir() {
return backupsDir;
}
public boolean isReadOnly() {
return sessionLockChannel == null;
}
@Override
public boolean back() {
closePage();
return true;
}
@Override
public void closePage() {
if (sessionLockChannel != null) {
try {
sessionLockChannel.close();
LOG.info("Releases the lock on world " + world.getFileName());
} catch (IOException e) {
LOG.warning("Failed to close session lock channel", e);
}
sessionLockChannel = null;
}
}
}
@@ -0,0 +1,127 @@
/*
* 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;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class ChunkBaseApp {
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
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"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
"1.20", "1.19.3", "1.19", "1.18", "1.16", "1.13", "1.9", "1.7"
};
public static final String[] NETHER_FORTRESS_GAME_VERSIONS = {
"1.18", "1.16", "1.7"
};
public static final String[] END_CITY_GAME_VERSIONS = {
"1.19", "1.13"
};
public static boolean isSupported(@NotNull World world) {
return world.getSeed() != null && world.getGameVersion() != null &&
GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo(MIN_GAME_VERSION) >= 0;
}
public static ChunkBaseApp newBuilder(String app, long seed) {
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
}
public static void openSeedMap(World world) {
assert isSupported(world);
newBuilder("seed-map", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS)
.open();
}
public static void openStrongholdFinder(World world) {
assert isSupported(world);
newBuilder("stronghold-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS)
.open();
}
public static void openNetherFortressFinder(World world) {
assert isSupported(world);
newBuilder("nether-fortress-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, NETHER_FORTRESS_GAME_VERSIONS)
.open();
}
public static void openEndCityFinder(World world) {
assert isSupported(world);
newBuilder("endcity-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, END_CITY_GAME_VERSIONS)
.open();
}
private final StringBuilder builder;
private ChunkBaseApp(StringBuilder builder) {
this.builder = builder;
}
public ChunkBaseApp add(String key, String value) {
builder.append('&').append(key).append('=').append(value);
return this;
}
public ChunkBaseApp addPlatform(GameVersionNumber gameVersion, boolean largeBiomes, String[] versionList) {
String version = null;
for (String candidateVersion : versionList) {
if (gameVersion.compareTo(candidateVersion) >= 0) {
version = candidateVersion;
break;
}
}
if (version == null) {
version = versionList[versionList.length - 1]; // Use the last version if no suitable version found
}
add("platform", "java_" + version.replace('.', '_') + (largeBiomes ? "_lb" : ""));
return this;
}
public void open() {
FXUtils.openLink(builder.toString());
}
@Override
public String toString() {
return builder.toString();
}
}
@@ -332,6 +332,7 @@ download=Download
download.hint=Install games and modpacks or download mods, resource packs, and worlds.
download.code.404=File "%s" not found on the remote server.
download.content=Addons
download.shader=Shaders
download.curseforge.unavailable=This HMCL build does not support access to CurseForge. Please use the official build to access CurseForge.
download.existing=The file cannot be saved because it already exists. You can click "Save As" to save the file elsewhere.
download.external_link=Visit Download Website
@@ -437,6 +438,7 @@ folder.shaderpacks=Shader Packs
folder.saves=Saves
folder.schematics=Schematics
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.
@@ -1061,7 +1063,7 @@ datapack.title=World [%s] - Datapacks
web.failed=Failed to load page
web.open_in_browser=Do you want to open this address in a browser:\n%s
web.view_in_browser=View in browser
web.view_in_browser=View all in browser
world=Worlds
world.add=Add World
@@ -1072,9 +1074,12 @@ world.backup.create.locked=The world is currently in use. Please close the game
world.backup.create.success=Successfully created a new backup: %s
world.backup.delete=Delete this backup
world.backup.processing=Backing up ...
world.backup.title=World [%s] - Backups
world.datapack=Manage Datapacks
world.datapack.1_13=Only Minecraft 1.13 or later supports datapacks.
world.chunkbase=Chunk Base
world.chunkbase.end_city=End City
world.chunkbase.seed_map=Seed Map
world.chunkbase.stronghold=Stronghold
world.chunkbase.nether_fortress=Nether Fortress
world.datapack=Datapacks
world.datetime=Last played on %s
world.download=Download World
world.download.title=Download World - %1s
@@ -1089,7 +1094,6 @@ world.import.choose=Choose world archive you want to import
world.import.failed=Failed to import this world: %s
world.import.invalid=Failed to parse the world.
world.info=World Information
world.info.title=World [%s] - Information
world.info.basic=Basic Information
world.info.allow_cheats=Allow Commands/Cheats
world.info.dimension.the_nether=The Nether
@@ -1120,6 +1124,8 @@ world.info.time=Game Time
world.info.time.format=%s days
world.locked=In use
world.manage=Worlds
world.manage.button=World Management
world.manage.title=World - %s
world.name=World Name
world.name.enter=Enter the world name
world.show_all=Show All
@@ -1331,6 +1337,7 @@ settings.memory.allocate.manual=%1$.1f GiB Allocated
settings.memory.allocate.manual.exceeded=%1$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.auto_allocate=Automatically Allocate
settings.memory.lower_bound=Minimum Memory
settings.memory.unit.mib=MiB
settings.memory.used_per_total=%1$.1f GiB Used / %2$.1f GiB Total
settings.physical_memory=Physical Memory Size
settings.show_log=Show Logs
@@ -333,6 +333,7 @@ download=Descargar
download.hint=Instalar juegos y modpacks o descargar mods, paquetes de recursos y mundos.
download.code.404=Archivo no encontrado en el servidor remoto: %s
download.content=Complementos
download.shader=Sombreadores
download.curseforge.unavailable=Esta versión de HMCL no permite acceder a CurseForge. Utilice la versión oficial para acceder a CurseForge.
download.existing=El archivo no se puede guardar porque ya existe. Puedes hacer clic en «Guardar como» para guardar el archivo en otro lugar.
download.external_link=Abrir sitio web
@@ -438,6 +439,7 @@ folder.mod=Mods
folder.resourcepacks=Paquetes de recursos
folder.shaderpacks=Paquetes de sombreados
folder.saves=Mundos
folder.schematics=Esquemas
folder.screenshots=Capturas de pantalla
game=Juegos
@@ -1076,9 +1078,7 @@ world.backup.create.locked=El mundo está actualmente en uso. Por favor, cierra
world.backup.create.success=Creada con éxito una nueva copia de seguridad: %s
world.backup.delete=Eliminar esta copia de seguridad
world.backup.processing=Creando nueva copia de seguridad ...
world.backup.title=Mundo [%s] - Copias de seguridad
world.datapack=Gestionar paquetes de datos
world.datapack.1_13=Sólo Minecraft 1.13 o posterior soporta paquetes de datos.
world.datetime=Jugado por última vez en %s
world.download=Descargar Mundo
world.download.title=Descargar mundo - %1s
@@ -1093,7 +1093,6 @@ world.import.choose=Elija el archivo de mundo que desea importar
world.import.failed=No se ha podido importar este mundo: %s
world.import.invalid=No se ha podido analizar el mundo.
world.info=Información del mundo
world.info.title=Mundo [%s] - Información
world.info.basic=Información básica
world.info.allow_cheats=Permitir comandos/trucos
world.info.dimension.the_nether=El Nether
@@ -1124,6 +1123,7 @@ world.info.time=Tiempo de juego
world.info.time.format=%s días
world.locked=En uso
world.manage=Mundos
world.manage.title=Mundos - %s
world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
world.show_all=Mostrar todo
@@ -1334,6 +1334,7 @@ settings.memory.allocate.manual=%1$.1f GiB Asignados
settings.memory.allocate.manual.exceeded=%1$.1f GiB Asignados (%3$.1f GiB Disponibles)
settings.memory.auto_allocate=Asignar automáticamente
settings.memory.lower_bound=Memoria mínima
settings.memory.unit.mib=MiB
settings.memory.used_per_total=%1$.1f GiB Utilizados / %2$.1f GiB Totales
settings.physical_memory=Tamaño de la memoria física
settings.show_log=Mostrar registros
@@ -289,6 +289,7 @@ download=ダウンロード
download.hint=ゲームや modpack をインストールするか、mod、リソース パック、マップをダウンロードします
download.code.404=リモートサーバーにファイルが見つかりません:%s
download.content=ゲームコンテンツ
download.shader=シェーダー
download.existing=ファイルは既に存在するため、保存できません。「名前を付けて保存」を選択して、ファイルを別の場所に保存できます。
download.external_link=ダウンロードサイトを開く
download.failed=%1$s のダウンロードに失敗しました、応答コード:%2$d
@@ -700,7 +701,6 @@ datapack.title=World %s -データパック
world=マップ
world.add=マップを追加(.zip)
world.datapack=データパックの管理
world.datapack.1_13=Minecraft1.13以降のバージョンのみがデータパックをサポートします。
world.datetime=最終ゲーム時刻:%s
world.download=ダウンロード
world.export=このマップをエクスポートする
@@ -713,7 +713,8 @@ world.import.already_exists=このマップはすでに存在しています。
world.import.choose=インポートするzipファイルを選択してください
world.import.failed=このマップをインポートできません:%s
world.import.invalid=無効なワールドzipファイル
world.manage=マップ / データパック
world.manage=マップ
world.manage.title=マップ - %s
world.name=マップ名
world.name.enter=マップ名を入力してください
world.show_all=すべて表示
@@ -867,13 +868,14 @@ settings.launcher.title_transparent=タイトルの透明性
settings.launcher.version_list_source=バージョンリストソース
settings.memory=ゲームメモリ
settings.memory.allocate.auto=最小 %1$.1f GB /実際の %2$.1f GB
settings.memory.allocate.auto.exceeded=最小 %1$.1f GB /実際の %2$.1f GB(%3$.1f GB使用可能)
settings.memory.allocate.manual=割り当て %1$.1f GB
settings.memory.allocate.manual.exceeded=割り当て %1$.1f GB(%3$.1f GB使用可能)
settings.memory.allocate.auto=最小 %1$.1f GiB /実際の %2$.1f GiB
settings.memory.allocate.auto.exceeded=最小 %1$.1f GiB /実際の %2$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate.manual=割り当て %1$.1f GiB
settings.memory.allocate.manual.exceeded=割り当て %1$.1f GiB(%3$.1f GiB使用可能)
settings.memory.auto_allocate=メモリサイズの自動割り当て
settings.memory.lower_bound=最小メモリ
settings.memory.used_per_total=%1$.1f GB使用済み/%2$.1fGB合計
settings.memory.unit.mib=MiB
settings.memory.used_per_total=%1$.1f GiB使用済み/%2$.1fGiB合計
settings.physical_memory=物理メモリサイズ
settings.show_log=ログを表示
settings.tabs.installers=Installers
@@ -334,6 +334,7 @@ download=Скачать
download.hint=Установить игры и модпаки или скачать моды, пакеты ресурсов и миры.
download.code.404=Файл «%s» не найден на удаленном сервере.
download.content=Аддоны
download.shader=Шейдеры
download.curseforge.unavailable=Эта сборка HMCL не поддерживает доступ к CurseForge. Пожалуйста, используйте официальную сборку для доступа к CurseForge.
download.existing=Файл существует и по этому не может быть сохранён. Можно использовать «Сохранить как», чтобы сохранить файл в другом месте.
download.external_link=Открыть сайт
@@ -436,6 +437,7 @@ folder.mod=Моды
folder.resourcepacks=Пакеты ресурсов
folder.shaderpacks=Пакеты шейдеров
folder.saves=Миры
folder.schematics=Схемы
folder.screenshots=Снимки экрана
game=Игры
@@ -534,7 +536,7 @@ game.crash.reason.jvm_32bit=Игра отказала из-за того, что
\n\
Если ваша ОС 64-разрядная, установите и используйте 64-разрядная Java. Если ваша ОС 32-разрядная, вам может потребоваться переустановить 64-разрядную ОС или приобрести более современный компьютер.\n\
\n\
Или вы можете отключить опцию «Автовыделение» в разделе «Глобальные настройки / Раздельные настройки для сборки → Оперативная память» и указать максимальный размер выделяемой памяти 1024 МБ или меньше.
Или вы можете отключить опцию «Автовыделение» в разделе «Глобальные настройки / Раздельные настройки для сборки → Оперативная память» и указать максимальный размер выделяемой памяти 1024 МиБ или меньше.
game.crash.reason.loading_crashed_forge=Игра отказала из-за мода «%1$s» (%2$s).\n\
\n\
Вы можете попробовать удалить или обновить его.
@@ -776,9 +778,9 @@ launch.advice.java8_51_1_13=Minecraft 1.13 может аварийно заве
launch.advice.java9=Вы не сможете запустить Minecraft 1.12 и ниже с Java 9 и более новыми версиями Java. Используйте вместо этого Java 8.
launch.advice.modded_java=Некоторые моды могут быть несовместимы с новыми версиями Java. Рекомендуется использовать Java %s для запуска Minecraft %s.
launch.advice.newer_java=Вы используете старую версию Java для запуска игры. Рекомендуется обновить Java 8, иначе некоторые моды могут привести к сбою игры.
launch.advice.not_enough_space=Вы выделили слишком много памяти, поскольку размер физической памяти составляет %d МБ, ваша игра может рухнуть.
launch.advice.not_enough_space=Вы выделили слишком много памяти, поскольку размер физической памяти составляет %d МиБ, ваша игра может рухнуть.
launch.advice.require_newer_java_version=Для текущей версии игры требуется Java %s, но мы не смогли ее найти. Хотите скачать ее сейчас?
launch.advice.too_large_memory_for_32bit=Вы выделили слишком много памяти, из-за 32-разрядной JRE ваша игра может рухнуть. Максимальный объем памяти для 32-разрядных систем составляет 1024 МБ.
launch.advice.too_large_memory_for_32bit=Вы выделили слишком много памяти, из-за 32-разрядной JRE ваша игра может рухнуть. Максимальный объем памяти для 32-разрядных систем составляет 1024 МиБ.
launch.advice.vanilla_linux_java_8=На Linux x86-64, Minecraft 1.12.2 и ниже может работать только на Java 8.\nВерсии Java 9 и выше не могут загружать 32-битные нативные библиотеки, такие как liblwjgl.so.
launch.advice.modlauncher8=Используемая вами версия Forge несовместима с текущей версией Java. Попробуйте обновить Forge.
launch.advice.vanilla_x86.translation=Minecraft еще не полностью поддерживается на вашей платформе, поэтому вы можете столкнуться с отсутствием функций или даже не сможете запустить игру.\n\
@@ -1075,9 +1077,7 @@ world.backup.create.locked=В настоящее время мир находи
world.backup.create.success=Успешно создано новое резервное копирование: %s
world.backup.delete=Удалить эту резервную копию
world.backup.processing=Создание новой резервной копии ...
world.backup.title=Мир [%s] - Резервные копии
world.datapack=Управлять наборами данных
world.datapack.1_13=Только Minecraft 1.13 или новее поддерживает наборы данных.
world.datetime=Последний запуск игры %s
world.download=Скачать мир
world.download.title=Скачать мир - %1s
@@ -1092,7 +1092,6 @@ world.import.choose=Выберите архив мира, который хот
world.import.failed=Не удалось импортировать этот мир\: %s
world.import.invalid=Не удалось разобрать мир.
world.info=Сведения о мире
world.info.title=Мир [%s] - Сведения
world.info.basic=Основные сведения
world.info.allow_cheats=Разрешить команды/читы
world.info.dimension.the_nether=Нижний мир
@@ -1123,6 +1122,7 @@ world.info.time=Время игры
world.info.time.format=%s дн.
world.locked=В эксплуатации
world.manage=Миры
world.manage.title=Миры - %s
world.name=Название мира
world.name.enter=Введите название мира
world.show_all=Показать все
@@ -1218,7 +1218,7 @@ settings.advanced.environment_variables=Переменные среды
settings.advanced.game_dir.default=По умолчанию («.minecraft/»)
settings.advanced.game_dir.independent=Раздельно («.minecraft/versions/<имя сборки>/», кроме assets и библиотек)
settings.advanced.java_permanent_generation_space=Пространство PermGen
settings.advanced.java_permanent_generation_space.prompt=в МБ
settings.advanced.java_permanent_generation_space.prompt=в МиБ
settings.advanced.jvm=Настройки Java VM
settings.advanced.jvm_args=Аргументы Java VM
settings.advanced.jvm_args.prompt=\ · Если параметр, введённый в «Аргументы Java VM», совпадает с параметром по умолчанию, он не будет добавлен.\n\
@@ -1328,13 +1328,14 @@ settings.launcher.turn_off_animations=Отключить анимацию (Пр
settings.launcher.version_list_source=Список версий
settings.memory=Оперативная память
settings.memory.allocate.auto=Минимум %1$.1f ГБ / Выделено %2$.1f ГБ
settings.memory.allocate.auto.exceeded=Минимум %1$.1f ГБ / Выделено %2$.1f ГБ (Доступно %3$.1f ГБ)
settings.memory.allocate.manual=Выделено %1$.1f ГБ
settings.memory.allocate.manual.exceeded=Выделено %1$.1f ГБ (Доступно %3$.1f ГБ)
settings.memory.allocate.auto=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ
settings.memory.allocate.auto.exceeded=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ (Доступно %3$.1f ГиБ)
settings.memory.allocate.manual=Выделено %1$.1f ГиБ
settings.memory.allocate.manual.exceeded=Выделено %1$.1f ГиБ (Доступно %3$.1f ГиБ)
settings.memory.auto_allocate=Автовыделение
settings.memory.lower_bound=Мин. объём памяти
settings.memory.used_per_total=Использовано %1$.1f ГБ / Всего %2$.1f ГБ
settings.memory.unit.mib=МиБ
settings.memory.used_per_total=Использовано %1$.1f ГиБ / Всего %2$.1f ГиБ
settings.physical_memory=Размер физической памяти
settings.show_log=Показать логи
settings.tabs.installers=Погрузчики
@@ -334,6 +334,7 @@ download=下載
download.hint=安裝遊戲和模組包或下載模組、資源包和地圖
download.code.404=遠端伺服器沒有需要下載的檔案:%s
download.content=遊戲內容
download.shader=光影
download.curseforge.unavailable=這個 HMCL 版本不支援訪問 CurseForge。請使用官方版本進行下載。
download.existing=檔案已存在,無法儲存。你可以將檔案儲存至其他地方。
download.external_link=開啟下載網站
@@ -408,6 +409,7 @@ folder.shaderpacks=著色器包目錄
folder.saves=遊戲存檔目錄
folder.schematics=原理圖目錄
folder.screenshots=截圖目錄
folder.world=世界目錄
game=遊戲
game.crash.feedback=<b>請不要將本介面截圖給他人!</b>如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。
@@ -863,7 +865,7 @@ datapack.title=世界 [%s] - 資料包
web.failed=加載頁面失敗
web.open_in_browser=是否要在瀏覽器中開啟此連結:\n%s
web.view_in_browser=在瀏覽器中查看
web.view_in_browser=在瀏覽器中查看完整日誌
world=世界
world.add=新增世界
@@ -874,9 +876,12 @@ world.backup.create.locked=該世界正在使用中,請關閉遊戲後重試
world.backup.create.success=成功創建新備份:%s
world.backup.delete=删除此備份
world.backup.processing=正在備份中……
world.backup.title=世界 [%s] - 備份
world.datapack=管理資料包
world.datapack.1_13=僅 Minecraft 1.13 及之後的版本支援資料包
world.chunkbase=世界地圖
world.chunkbase.end_city=終界城地圖
world.chunkbase.seed_map=種子地圖
world.chunkbase.stronghold=要塞地圖
world.chunkbase.nether_fortress=地獄要塞地圖
world.datapack=資料包管理
world.datetime=上一次遊戲時間: %s
world.download=下載世界
world.download.title=世界下載 - %1s
@@ -890,7 +895,6 @@ world.import.choose=選取要匯入的存檔壓縮檔
world.import.failed=無法匯入此世界: %s
world.import.invalid=無法識別的存檔壓縮檔
world.info=世界資訊
world.info.title=世界 [%s] - 世界資訊
world.info.basic=基本資訊
world.info.allow_cheats=允許指令(作弊)
world.info.dimension.the_nether=地獄
@@ -921,7 +925,9 @@ world.info.time=遊戲內時間
world.info.time.format=%s 天
world.locked=使用中
world.game_version=遊戲版本
world.manage=世界/資料包
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.name=世界名稱
world.name.enter=輸入世界名稱
world.show_all=全部顯示
@@ -1124,6 +1130,7 @@ settings.memory.allocate.manual=遊戲分配 %1$.1f GiB
settings.memory.allocate.manual.exceeded=遊戲分配 %1$.1f GiB (%3$.1f GiB 可用)
settings.memory.auto_allocate=自動分配
settings.memory.lower_bound=最低分配
settings.memory.unit.mib=MiB
settings.memory.used_per_total=已使用 %1$.1f GiB / 總記憶體 %2$.1f GiB
settings.physical_memory=實體記憶體大小
settings.show_log=查看日誌
@@ -342,6 +342,7 @@ download=下载
download.hint=安装游戏和整合包或下载模组、资源包和地图
download.code.404=远程服务器不包含需要下载的文件: %s\n你可以点击右上角帮助按钮进行求助。
download.content=游戏内容
download.shader=光影
download.curseforge.unavailable=此 HMCL 版本不支持访问 CurseForge。请使用官方版本进行下载。
download.existing=文件已存在,无法保存。你可以将文件保存至其他地方。
download.external_link=打开下载网站
@@ -417,6 +418,7 @@ folder.shaderpacks=光影包文件夹
folder.saves=存档文件夹
folder.schematics=原理图文件夹
folder.screenshots=截图文件夹
folder.world=世界文件夹
game=游戏
game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要向他人求助,请你点击左下角<b>“导出游戏崩溃信息”</b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b>“帮助”</b>前往交流群寻求帮助。
@@ -873,20 +875,23 @@ datapack.title=世界 [%s] - 数据包
web.failed=加载页面失败
web.open_in_browser=是否要在浏览器中打开此链接:\n%s
web.view_in_browser=在浏览器中查看
web.view_in_browser=在浏览器中查看完整日志
world=世界
world.add=添加世界
world.backup=世界备份
world.backup=备份管理
world.backup.create.new_one=创建新备份
world.backup.create.failed=创建备份失败。\n%s
world.backup.create.locked=该世界正在使用中,请关闭游戏后重试。
world.backup.create.success=成功创建新备份:%s
world.backup.delete=删除此备份
world.backup.processing=正在备份中……
world.backup.title=世界 [%s] - 备份
world.datapack=管理数据包
world.datapack.1_13=仅 Minecraft 1.13 及之后的版本支持数据包
world.chunkbase=世界地图
world.chunkbase.end_city=末地城地图
world.chunkbase.seed_map=种子地图
world.chunkbase.stronghold=要塞地图
world.chunkbase.nether_fortress=下界要塞地图
world.datapack=数据包管理
world.datetime=上一次游戏时间: %s
world.download=下载世界
world.download.title=世界下载 - %1s
@@ -901,7 +906,6 @@ world.import.choose=选择要导入的存档压缩包
world.import.failed=无法导入此世界:%s
world.import.invalid=无法识别该存档压缩包
world.info=世界信息
world.info.title=世界 [%s] - 世界信息
world.info.basic=基本信息
world.info.allow_cheats=允许命令(作弊)
world.info.dimension.the_nether=下界
@@ -931,7 +935,9 @@ world.info.random_seed=种子
world.info.time=游戏内时间
world.info.time.format=%s 天
world.locked=使用中
world.manage=世界/数据包
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.name=世界名称
world.name.enter=输入世界名称
world.show_all=显示全部
@@ -1134,6 +1140,7 @@ settings.memory.allocate.manual=游戏分配 %1$.1f GiB
settings.memory.allocate.manual.exceeded=游戏分配 %1$.1f GiB (设备仅 %3$.1f GiB 可用)
settings.memory.auto_allocate=自动分配内存
settings.memory.lower_bound=最低内存分配
settings.memory.unit.mib=MiB
settings.memory.used_per_total=设备中已使用 %1$.1f GiB / 设备总内存 %2$.1f GiB
settings.physical_memory=物理内存大小
settings.show_log=查看日志
@@ -24,6 +24,7 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.util.io.*;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
@@ -31,6 +32,7 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
@@ -41,7 +43,7 @@ import java.util.zip.GZIPOutputStream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class World {
public final class World {
private final Path file;
private String fileName;
@@ -49,6 +51,8 @@ public class World {
private String gameVersion;
private long lastPlayed;
private Image icon;
private Long seed;
private boolean largeBiomes;
private boolean isLocked;
public World(Path file) throws IOException {
@@ -65,7 +69,7 @@ public class World {
private void loadFromDirectory() throws IOException {
fileName = FileUtils.getName(file);
Path levelDat = file.resolve("level.dat");
getWorldName(levelDat);
loadWorldInfo(levelDat);
isLocked = isLocked(getSessionLockFile());
Path iconFile = file.resolve("icon.png");
@@ -108,6 +112,14 @@ public class World {
return gameVersion;
}
public @Nullable Long getSeed() {
return seed;
}
public boolean isLargeBiomes() {
return largeBiomes;
}
public Image getIcon() {
return icon;
}
@@ -121,7 +133,7 @@ public class World {
if (!Files.exists(levelDat))
throw new IOException("Not a valid world zip file since level.dat cannot be found.");
getWorldName(levelDat);
loadWorldInfo(levelDat);
Path iconFile = root.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
@@ -154,7 +166,7 @@ public class World {
}
}
private void getWorldName(Path levelDat) throws IOException {
private void loadWorldInfo(Path levelDat) throws IOException {
CompoundTag nbt = parseLevelDat(levelDat);
CompoundTag data = nbt.get("Data");
@@ -178,6 +190,27 @@ public class World {
if (version.get("Name") instanceof StringTag)
gameVersion = version.<StringTag>get("Name").getValue();
}
Tag worldGenSettings = data.get("WorldGenSettings");
if (worldGenSettings instanceof CompoundTag) {
Tag seedTag = ((CompoundTag) worldGenSettings).get("seed");
if (seedTag instanceof LongTag) {
seed = ((LongTag) seedTag).getValue();
}
}
if (seed == null) {
Tag seedTag = data.get("RandomSeed");
if (seedTag instanceof LongTag) {
seed = ((LongTag) seedTag).getValue();
}
}
// FIXME: Only work for 1.15 and below
if (data.get("generatorName") instanceof StringTag) {
largeBiomes = "largeBiomes".equals(data.<StringTag>get("generatorName").getValue());
} else {
largeBiomes = false;
}
}
public void rename(String newName) throws IOException {
@@ -293,7 +326,7 @@ public class World {
private static boolean isLocked(Path sessionLockFile) {
try (FileChannel fileChannel = FileChannel.open(sessionLockFile, StandardOpenOption.WRITE)) {
return fileChannel.tryLock() == null;
} catch (AccessDeniedException accessDeniedException) {
} catch (AccessDeniedException | OverlappingFileLockException accessDeniedException) {
return true;
} catch (NoSuchFileException noSuchFileException) {
return false;
@@ -46,6 +46,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
public static final ModrinthRemoteModRepository MODS = new ModrinthRemoteModRepository("mod");
public static final ModrinthRemoteModRepository MODPACKS = new ModrinthRemoteModRepository("modpack");
public static final ModrinthRemoteModRepository RESOURCE_PACKS = new ModrinthRemoteModRepository("resourcepack");
public static final ModrinthRemoteModRepository SHADER_PACKS = new ModrinthRemoteModRepository("shader");
private static final String PREFIX = "https://api.modrinth.com";
@@ -45,6 +45,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -96,7 +97,10 @@ public final class MultiMCInstancePatch {
@SerializedName("jarMods")
private final List<Library> jarMods;
public MultiMCInstancePatch(int formatVersion, String id, String version, AssetIndexInfo assetIndex, String minecraftArguments, List<String> jvmArgs, String mainClass, int[] javaMajors, Library mainJar, List<String> traits, List<String> tweakers, List<Library> libraries0, List<Library> libraries1, List<Library> mavenFiles, List<Library> jarMods) {
@SerializedName("requires")
private final List<MultiMCManifest.MultiMCManifestCachedRequires> requires;
public MultiMCInstancePatch(int formatVersion, String id, String version, AssetIndexInfo assetIndex, String minecraftArguments, List<String> jvmArgs, String mainClass, int[] javaMajors, Library mainJar, List<String> traits, List<String> tweakers, List<Library> libraries0, List<Library> libraries1, List<Library> mavenFiles, List<Library> jarMods, List<MultiMCManifest.MultiMCManifestCachedRequires> requires) {
this.formatVersion = formatVersion;
this.id = id;
this.version = version;
@@ -112,6 +116,7 @@ public final class MultiMCInstancePatch {
this.libraries1 = libraries1;
this.mavenFiles = mavenFiles;
this.jarMods = jarMods;
this.requires = requires;
}
public int getFormatVersion() {
@@ -177,6 +182,10 @@ public final class MultiMCInstancePatch {
return nonNullOrEmpty(jarMods);
}
public List<MultiMCManifest.MultiMCManifestCachedRequires> getRequires() {
return nonNullOrEmpty(requires);
}
private static <T> List<T> nonNullOrEmpty(List<T> value) {
return value != null && !value.isEmpty() ? value : Collections.emptyList();
}
@@ -257,6 +266,8 @@ public final class MultiMCInstancePatch {
}
for (MultiMCInstancePatch patch : patches) {
Objects.requireNonNull(patch, "patch");
if (patch.getFormatVersion() != 1) {
throw new UnsupportedOperationException(
String.format("Unsupported JSON-Patch[%s] format version: %d", patch.getID(), patch.getFormatVersion())
@@ -86,7 +86,7 @@ public final class MultiMCManifest {
return equalsVersion;
}
public String getUid() {
public String getID() {
return uid;
}
@@ -44,16 +44,26 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* <p>A task transforming MultiMC Modpack Scheme to Official Launcher Scheme.
* The transforming process contains 7 stage:
* General Setup, Load Components, Resolve Json-Patch, Build Artifact,
* Copy Embedded Files, Assemble Game, Download Game and Apply JAR mods.
* <ul>
* <li>General Setup</li>
* <li>Load Components</li>
* <li>Resolve Json-Patch</li>
* <li>Build Artifact</li>
* <li>Copy Embedded Files</li>
* <li>Assemble Game</li>
* <li>Download Game</li>
* <li>Apply JAR mods</li>
* </ul>
* See codes below for detailed implementation.
* </p>
*/
@@ -64,7 +74,6 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
private final MultiMCInstanceConfiguration manifest;
private final String name;
private final DefaultGameRepository repository;
private final List<Task<MultiMCInstancePatch>> patches = new ArrayList<>();
private final List<Task<?>> dependents = new ArrayList<>();
private final List<Task<?>> dependencies = new ArrayList<>();
private final DefaultDependencyManager dependencyManager;
@@ -125,29 +134,74 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
try (FileSystem fs = openModpack()) {
Path root = getRootPath(fs);
List<Task<MultiMCInstancePatch>> patches = new ArrayList<>();
for (MultiMCManifest.MultiMCManifestComponent component : Objects.requireNonNull(
Objects.requireNonNull(manifest.getMmcPack(), "mmc-pack.json").getComponents(), "components"
)) {
String componentID = Objects.requireNonNull(component.getUid(), "Component ID");
Path patchPath = root.resolve(String.format("patches/%s.json", componentID));
Task<String> task;
if (Files.exists(patchPath)) {
if (!Files.isRegularFile(patchPath)) {
throw new IllegalArgumentException("Json-Patch isn't a file: " + componentID);
}
// TODO: Task.completed has unclear compatibility issue.
String text = FileUtils.readText(patchPath, StandardCharsets.UTF_8);
task = Task.supplyAsync(() -> text);
MultiMCInstancePatch patch = MultiMCInstancePatch.read(componentID, FileUtils.readText(patchPath, StandardCharsets.UTF_8));
patches.add(Task.supplyAsync(() -> patch)); // TODO: Task.completed has unclear compatibility issue.
} else {
task = new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion()));
patches.add(
new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion()))
.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s))
);
}
}
dependents.add(new MMCInstancePatchesAssembleTask(patches));
}
}
private static final class MMCInstancePatchesAssembleTask extends Task<List<MultiMCInstancePatch>> {
private final List<Task<MultiMCInstancePatch>> patches;
public MMCInstancePatchesAssembleTask(List<Task<MultiMCInstancePatch>> patches) {
this.patches = patches;
}
@Override
public Collection<? extends Task<?>> getDependents() {
return patches;
}
@Override
public void execute() throws Exception {
Map<String, MultiMCInstancePatch> existed = new HashMap<>();
for (Task<MultiMCInstancePatch> patch : patches) {
MultiMCInstancePatch result = patch.getResult();
existed.put(result.getID(), result);
}
checking:
while (true) {
for (MultiMCInstancePatch patch : existed.values()) {
for (MultiMCManifest.MultiMCManifestCachedRequires require : patch.getRequires()) {
String componentID = require.getID();
if (!existed.containsKey(componentID)) {
Task<MultiMCInstancePatch> task = new GetTask(MultiMCComponents.getMetaURL(
componentID, Lang.requireNonNullElse(require.getEqualsVersion(), require.getSuggests())
)).thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s));
task.run();
MultiMCInstancePatch result = Objects.requireNonNull(task.getResult());
existed.put(result.getID(), result);
continue checking;
}
}
}
Task<MultiMCInstancePatch> task2 = task.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s));
patches.add(task2);
dependents.add(task2);
break;
}
setResult(new ArrayList<>(existed.values()));
}
}
@@ -160,10 +214,15 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
@Override
public void execute() throws Exception {
// Stage #3: Build Json-Patch artifact.
MultiMCInstancePatch.ResolvedInstance artifact = MultiMCInstancePatch.resolveArtifact(patches.stream()
.map(value -> Objects.requireNonNull(value.getResult(), "MultiMCInstancePatch"))
.collect(Collectors.toList()), name
);
MultiMCInstancePatch.ResolvedInstance artifact = null;
for (int i = dependents.size() - 1; i >= 0; i--) {
Task<?> task = dependents.get(i);
if (task instanceof MMCInstancePatchesAssembleTask) {
artifact = MultiMCInstancePatch.resolveArtifact(((MMCInstancePatchesAssembleTask) task).getResult(), name);
break;
}
}
Objects.requireNonNull(artifact, "artifact");
// Stage #4: Copy embedded files.
try (FileSystem fs = openModpack()) {