Compare commits

..
Author SHA1 Message Date
庐庐绫 d297417909 fix: show/hide 前先 setIconified(false) 确保窗口状态正常 (#6525)
close #6523
2026-07-28 09:40:06 +08:00
Glavo 2854eac27d 在高刷屏上自动提高动画帧率 (#6422) 2026-07-27 21:30:27 +08:00
ToobLac c9fd7c6a1c [Enhancement] 优化主题包导出对话框的交互 (#6516)
Resolves #6512
2026-07-27 21:03:54 +08:00
Glavo 0e0517ab3c 优化模组信息读取速度 (#6515) 2026-07-27 07:12:00 +08:00
ToobLacandGlavo 7a4bea10d0 [Bugfix] 修复 #6348 资源包管理处理非UTF-8编码的options.txt文件时出错 (#6351)
Fixes #6348

---------

Co-authored-by: Glavo <[email protected]>
2026-07-26 21:31:35 +08:00
辞庐 b87d40ffcc 优化 实例列表无版本时点击下载页展示 (#6494) 2026-07-26 20:56:41 +08:00
Glavo 1277bf8b5d 使用 kala-encoding-detector 作为编码推测库 (#6477) 2026-07-26 00:47:43 +08:00
辞庐 c2e807055a fix: 下载整合包时未设置任务名 (#6487) 2026-07-25 21:07:43 +08:00
辞庐 9021ec1960 清理模组管理页面冗余 “实例无模组加载器” 提示 (#6500) 2026-07-25 21:01:22 +08:00
辞庐 82a47e9e2d 优化实例列表界面 (#6493) 2026-07-25 21:00:42 +08:00
neveler 4841204fa0 update i18n (#6491) 2026-07-25 21:00:26 +08:00
辞庐 897f5fe11b fix: 形参化类 'Tab<?>' 的原始使用 (#6498) 2026-07-25 20:57:58 +08:00
Glavo 4657e3273e 复制实例时同步保存游戏设置 (#6492) 2026-07-24 20:39:59 +08:00
Glavo 4edea5f5b9 Add support for Minecraft 26.3-snapshot-5+ to unofficial supported platforms (#6481) 2026-07-23 19:51:00 +08:00
辞庐 36bd7d4491 修复 GameAdvancedListItem#loadVersion UI 操作可能不会 JavaFX 线程运行的问题 (#6445)
resolves #6443
2026-07-22 22:11:55 +08:00
neveler 3aa8603bd1 提取 pr-size-label 工作流中的 js 代码到独立文件 (#6458) 2026-07-22 22:06:57 +08:00
庐庐绫 ca7a61e20a fix: 修复模组列表加载时检查错误实例的模组更新 (#6478)
close #6448
2026-07-22 21:57:59 +08:00
辞庐 21afc81cf8 fix: NetwordUtils#doPost(URI u, Map<String, String> params) 未进行 URL 编码 (#6457) 2026-07-22 21:49:46 +08:00
neveler 94a0e26be5 update i18n: remove game.crash.reason.mod_repeat_installation (#6472) 2026-07-22 21:47:13 +08:00
辞庐 14e8d2d0b5 清理 VanillaInstallWizardProvider (#6474) 2026-07-22 21:46:49 +08:00
快乐凤爪王 49c408cb23 Refactor: InstallersPageSkin versionNamePane (#6349) 2026-07-22 21:45:49 +08:00
辞庐 b81c3398ac fix: 切换版本隔离设置后自动刷新其他子页面 功能失效 (#6468) 2026-07-22 13:21:58 +08:00
快乐凤爪王 2855df3693 Feat: Implement dynamic folder icon and use to game directory list (#6435) 2026-07-21 22:03:32 +08:00
快乐凤爪王 8184dc6cee Fix: Alignment for background opacity percentage label (#6450) 2026-07-21 21:55:12 +08:00
快乐凤爪王 df21559c0e Chore: Clean up duplicate key-value pairs for No results found (#6449) 2026-07-21 21:53:21 +08:00
快乐凤爪王 73ff3081f8 Feat: No results found tip for ThemePackManagementPage (#6451) 2026-07-21 21:49:52 +08:00
辞庐 fe05bb58a4 更新时 日志输出新进程命令行时 打码敏感信息 (#6456) 2026-07-21 21:30:32 +08:00
Glavo 4c9cd950e4 移除对 JavaFX 14 的兼容性 (#6467) 2026-07-21 20:40:56 +08:00
neveler 646d9a09a5 update i18n (#6466) 2026-07-21 20:38:56 +08:00
Glavo 255fe75640 Bump JavaFX to 21.0.11 on FreeBSD x86-64 (#6463) 2026-07-21 19:40:18 +08:00
neveler de15dd8e98 Remove extra whitespace (#6462) 2026-07-21 17:57:57 +08:00
Glavo 886319a6c7 避免在代码中直接引用 Controllers.getStage() (#6431) 2026-07-20 21:41:55 +08:00
辞庐 3320f6c341 fix: 整合包安装完成回调执行器不统一 (#6446) 2026-07-20 21:21:33 +08:00
快乐凤爪王 b85be78166 Feat: Add empty state placeholders for versions (#6426) 2026-07-20 21:21:06 +08:00
Glavo a34a7439d7 Clarify nullability and documentation in AGENTS.md (#6440) 2026-07-20 00:02:59 +08:00
ZZZank 591fd51600 实现游戏日志窗口开关自动换行 (#6424) 2026-07-19 21:53:57 +08:00
快乐凤爪王 c1407a9620 Fix: Resolve ModLoaderType.LEGACY_FABRIC for Modrinth (#6434) 2026-07-19 02:46:28 +08:00
Glavo e0a91579f2 更新内置主题相关译文 (#6429) 2026-07-17 21:29:51 +08:00
快乐凤爪王 b68f404c3c Refactor: Style of ModInfoDialog (#6423) 2026-07-17 21:28:02 +08:00
Glavo c1f50f9ba3 删除 Gemini 审查配置文件 (#6430) 2026-07-17 21:17:31 +08:00
辞庐 d8bb2131f2 允许拖入 nbt 文件 (#6427) 2026-07-17 21:08:45 +08:00
Glavo aca761385b Add support for Minecraft 26.3-snapshot-4+ to unofficial supported platforms (#6425) 2026-07-17 21:00:14 +08:00
3gf8jv4dv 15faccf376 chore(workflow): add pr-size-label workflow file (#6381) 2026-07-17 20:23:48 +08:00
Glavo 241c9c419c 修复 FXUtils 意外的被提前初始化的问题 (#6421) 2026-07-16 21:29:12 +08:00
辞庐 4b36f8ce03 fix: 任务窗口速度与取消未对齐 (#6140) 2026-07-16 21:28:31 +08:00
Glavo 5c0baad782 提早加载配置文件 (#6420) 2026-07-16 20:50:20 +08:00
Apostrophe 850a56405c 修复游戏 Java 路径显示滞后问题 (#6419) 2026-07-16 19:59:17 +08:00
快乐凤爪王 e8487c467b Refactor: Replace deprecated URL constructor (#6409) 2026-07-16 03:37:54 +08:00
Glavo 9866d88a16 完善游戏设置页面帮助文本 (#6415) 2026-07-15 21:15:00 +08:00
辞庐 af2d46ae89 Revert "Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin" (#6410)
Reverts HMCL-dev/HMCL#6370
2026-07-15 19:56:20 +08:00
ToobLac 6f7c561ecb [Bugfix] 修复 1.21.1 安装信雅互联仍显示不支持 Fabric 模组的问题 (#6404)
Fixes #6391
2026-07-14 21:41:39 +08:00
ToobLac 93abe31acf [Chores] 处理升级到 9.6.1 后 Gradle 弃用提示 (#6405) 2026-07-14 21:29:58 +08:00
Glavo a6f8659349 Bump Gradle Wrapper Neo to 0.2.0 (#6397) 2026-07-14 00:06:56 +08:00
Glavo 8728c89093 Bump version root to 3.17 (#6392) 2026-07-13 20:34:38 +08:00
99 changed files with 2090 additions and 6613 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
code_review:
pull_request_opened:
code_review: false
+105
View File
@@ -0,0 +1,105 @@
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const ignoredFilePatterns = [
/(?:^|\/)\.cnb\/.+\.ya?ml$/i,
/(?:^|\/)\.gitee\/.+\.ya?ml$/i,
/(?:^|\/)\.github\/.+\.ya?ml$/i,
/(?:^|\/)\.idea\/.+\.xml$/i,
/(?:^|\/)\.editorconfig$/i,
/(?:^|\/)\.gitattributes$/i,
/(?:^|\/)\.gitignore$/i,
/(?:^|\/).+\.kts$/i,
/(?:^|\/).+\.md$/i,
/(?:^|\/)LICENSE(?:_.*)?$/i,
/(?:^|\/)gradlew(?:\.bat|\.ps1)?$/i,
/(?:^|\/)gradle\/wrapper\/gradle-wrapper\.properties$/i,
/(?:^|\/)lang\/.+\.properties$/i,
/(?:^|\/)l10n\/.+\.properties$/i,
/(?:^|\/)checkstyle\.xml$/i,
/(?:^|\/).+\.(?:png|jpe?g|gif|ico|svg|json|ttf|woff2?|webp|avif|jxl|heic|mp4|webm|mp3|ogg|wav|txt|toml)$/i
];
async function processPR(issueNumber) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: issueNumber, per_page: 100,
});
const countedFiles = [];
const ignoredFiles = [];
let totalChanges = 0;
for (const file of files) {
const ignored = ignoredFilePatterns.some(pattern => pattern.test(file.filename));
if (ignored) {
ignoredFiles.push(file.filename);
continue;
}
const fileChanges = file.changes ?? ((file.additions || 0) + (file.deletions || 0));
totalChanges += fileChanges;
countedFiles.push(`${file.filename} (${fileChanges})`);
}
let targetLabel;
if (totalChanges <= 9) targetLabel = "1+";
else if (totalChanges <= 39) targetLabel = "10+";
else if (totalChanges <= 99) targetLabel = "40+";
else if (totalChanges <= 499) targetLabel = "100+";
else if (totalChanges <= 999) targetLabel = "500+";
else if (totalChanges <= 1999) targetLabel = "1000+";
else if (totalChanges <= 4999) targetLabel = "2000+";
else targetLabel = "5000+";
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner, repo, issue_number: issueNumber, per_page: 100,
});
const sizeLabels = currentLabels.map(label => label.name).filter(name => /^\d+\+$/.test(name));
for (const name of sizeLabels) {
if (name !== targetLabel) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: issueNumber, name,
}).catch(() => { });
}
}
if (!currentLabels.some(label => label.name === targetLabel)) {
await github.rest.issues.addLabels({
owner, repo, issue_number: issueNumber, labels: [targetLabel],
});
}
core.info(`Pull request #${issueNumber} has ${totalChanges} counted changed lines, applied ${targetLabel}.`);
core.info(`Ignored files: ${ignoredFiles.length ? ignoredFiles.join(", ") : "none"}`);
core.info(`Counted files: ${countedFiles.length ? countedFiles.join(", ") : "none"}`);
}
if (context.eventName === "workflow_dispatch") {
core.info("Manual trigger detected. Fetching all open pull requests...");
const openPRs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: "open", per_page: 100,
});
const unlabelledPRs = openPRs.filter(pr => {
return !pr.labels.some(label => /^\d+\+$/.test(label.name));
});
core.info(`Found ${openPRs.length} open pull request(s), ${unlabelledPRs.length} of them lack a size label.`);
for (const pr of unlabelledPRs) {
core.info(`----------------------------------------`);
core.info(`Processing unlabelled pull request #${pr.number}: ${pr.title}`);
await processPR(pr.number);
}
} else {
const pr = context.payload.pull_request;
if (!pr) {
core.info("No pull request found in payload, skipping.");
return;
}
await processPR(pr.number);
}
}
+31
View File
@@ -0,0 +1,31 @@
name: PR Size Label
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v7
with:
sparse-checkout: |
.github/scripts
sparse-checkout-cone-mode: true
- name: Label PR by filtered changed lines
uses: actions/github-script@v9
with:
script: |
const script = require("./.github/scripts/pr-size-label.js");
await script({ github, context, core });
+5 -3
View File
@@ -4,9 +4,9 @@ These rules apply to all Java code written or modified in this repository.
## Nullability
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability must never be implicit.
- Every class declared in a newly added Java source file must be annotated with JetBrains Annotations `@NotNullByDefault`.
- When writing or modifying Java code, any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability in code being written or modified must never be implicit.
## Immutability
@@ -15,6 +15,8 @@ These rules apply to all Java code written or modified in this repository.
## Documentation
Apply the following requirements when writing or modifying code. Do not use them as code-review criteria.
- Every class, field, and method must have documentation.
- Documentation must use `///` Markdown-style Javadoc comments.
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
+7 -7
View File
@@ -52,7 +52,7 @@ if (buildNumber != null) {
}
}
val embedResources by configurations.registering
val embedResources = configurations.register("embedResources")
dependencies {
implementation(project(":HMCLCore"))
@@ -166,7 +166,7 @@ val hmclProperties = buildList {
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile by tasks.registering {
val createPropertiesFile = tasks.register("createPropertiesFile") {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
@@ -258,7 +258,7 @@ tasks.processResources {
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
val makeExecutables by tasks.registering {
val makeExecutables = tasks.register("makeExecutables") {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
@@ -286,7 +286,7 @@ val makeExecutables by tasks.registering {
}
}
val makeDeb by tasks.registering(CreateDeb::class) {
val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
dependsOn(makeExecutables)
val debFile = layout.file(provider { artifactFile("deb") })
@@ -438,19 +438,19 @@ tasks.register<CheckTranslations>("checkTranslations") {
val generatedDir = layout.buildDirectory.dir("generated")
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
}
val createLanguageList by tasks.registering(CreateLanguageList::class) {
val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList") {
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
resourceBundleBaseName.set("I18N")
additionalLanguages.set(listOf("en-Qabs"))
outputFile.set(generatedDir.map { it.file("languages.json") })
}
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
val createLocaleNamesResourceBundle = tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
dependsOn(createLanguageList)
languagesFile.set(createLanguageList.flatMap { it.outputFile })
@@ -52,8 +52,6 @@ public final class EntryPoint {
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
if (!isInsideMacAppBundle())
@@ -74,71 +72,6 @@ public final class EntryPoint {
System.exit(exitCode);
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_LOCAL_HOME)) {
try {
@@ -225,15 +158,8 @@ public final class EntryPoint {
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
showWarning(i18n("fatal.wine_warning"));
}
}
@@ -273,10 +199,21 @@ public final class EntryPoint {
}
}
static void showWarning(String message) {
SwingUtils.initLookAndFeel();
int result = JOptionPane.showOptionDialog(null, message, i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
static void showErrorAndExit(String message) {
SwingUtils.showErrorDialog(message);
exit(1);
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl;
import com.sun.jna.Pointer;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
@@ -29,8 +30,6 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
@@ -48,7 +47,13 @@ import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.Gdi32;
import org.jackhuang.hmcl.util.platform.windows.User32;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
@@ -60,7 +65,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -98,18 +102,7 @@ public final class Launcher extends Application {
}
try {
try {
SettingsManager.init();
initializeSettingsRuntime();
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
EntryPoint.exit(1);
}
initializeSettingsRuntime();
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
@@ -274,52 +267,10 @@ public final class Launcher extends Application {
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
Path configDirectory = SettingsManager.localConfigDirectory();
if (!Files.exists(configDirectory)) {
return;
}
String owner;
try {
owner = Files.getOwner(configDirectory).getName();
} catch (IOException ioe) {
LOG.warning("Failed to get file owner", ioe);
return;
}
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(configDirectory.toString());
if (Files.exists(Metadata.HMCL_USER_HOME))
files.add(Metadata.HMCL_USER_HOME.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
if (showAlert(AlertType.ERROR,
i18n("fatal.config_loading_failure.unix", owner, command),
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
EntryPoint.exit(1);
}
@Override
public void stop() throws Exception {
Controllers.onApplicationStop();
FileSaver.shutdown();
SettingsManager.shutdown();
LOG.shutdown();
}
@@ -376,6 +327,19 @@ public final class Launcher extends Application {
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
try {
SettingsManager.init();
} catch (SambaException e) {
EntryPoint.showWarning(i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigOwner();
SwingUtils.showErrorDialog(i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
EntryPoint.exit(1);
}
setupJavaFXVMOptions();
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
@@ -408,5 +372,158 @@ public final class Launcher extends Application {
});
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
setUpAnimationFrameRate:
{
if (System.getProperty("javafx.animation.pulse") != null) {
break setUpAnimationFrameRate;
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
int value = Integer.parseInt(animationFrameRate);
if (value <= 0)
throw new NumberFormatException(animationFrameRate);
if (value != 60)
System.setProperty("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
break setUpAnimationFrameRate;
}
// To avoid prematurely loading FXUtils, we only check if animationDisabled has been explicitly set to true
if (Boolean.TRUE.equals(settings().animationDisabledProperty().get()))
break setUpAnimationFrameRate;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (NativeUtils.USE_JNA && Gdi32.INSTANCE != null && User32.INSTANCE != null) {
@Nullable Pointer pointer = User32.INSTANCE.GetDC(Pointer.NULL);
if (pointer != null) {
try {
int refreshRate = Gdi32.INSTANCE.GetDeviceCaps(pointer, Gdi32.VREFRESH);
if (refreshRate > 0) {
LOG.info("Detected refresh rate: " + refreshRate + "Hz");
if (refreshRate >= 90) {
System.getProperties().putIfAbsent("javafx.animation.pulse", String.valueOf(refreshRate));
}
}
} finally {
User32.INSTANCE.ReleaseDC(Pointer.NULL, pointer);
}
}
}
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
Path configDirectory = SettingsManager.localConfigDirectory();
if (!Files.exists(configDirectory)) {
return;
}
String owner;
try {
owner = Files.getOwner(configDirectory).getName();
} catch (IOException ioe) {
LOG.warning("Failed to get file owner", ioe);
return;
}
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(configDirectory.toString());
if (Files.exists(Metadata.HMCL_USER_HOME))
files.add(Metadata.HMCL_USER_HOME.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
SwingUtils.initLookAndFeel();
Object[] options = {i18n("button.copy_and_exit"), i18n("button.cancel")};
int result = JOptionPane.showOptionDialog(null,
i18n("fatal.config_loading_failure.unix", owner, command),
i18n("message.error"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE,
null,
options,
options[0]);
if (result == 0) {
try {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(command), null);
} catch (Throwable e) {
LOG.warning("Failed to copy command to clipboard", e);
}
}
EntryPoint.exit(1);
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -760,7 +761,10 @@ public final class LauncherHelper {
case HIDE_AND_REOPEN:
runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
.ifPresent(stage -> {
stage.setIconified(false);
stage.show();
});
});
break;
case KEEP:
@@ -895,8 +899,10 @@ public final class LauncherHelper {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().hide();
@Nullable Stage stage = Controllers.getStage();
if (stage != null) {
stage.setIconified(false);
stage.hide();
launchingLatch.countDown();
}
});
@@ -17,8 +17,8 @@
*/
package org.jackhuang.hmcl.game;
import kala.encdet.EncodingDetector;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -91,7 +91,7 @@ public final class LogExporter {
for (Path file : stream) {
if (Files.isRegularFile(file)) {
if (logMatcher == null || logMatcher.matches(file)) {
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
try (BufferedReader reader = EncodingDetector.MODERN_WEB.newBufferedReader(file)) {
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
@@ -231,7 +231,7 @@ public final class ModpackHelper {
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
@@ -34,8 +34,6 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Objects;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
/// Persistent configuration for a game directory.
@JsonAdapter(GameDirectory.Serializer.class)
@NotNullByDefault
@@ -118,7 +116,7 @@ public final class GameDirectory implements Observable {
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
this.legacyGameSettings = new SimpleObjectProperty<>(this, "legacyGameSettings", legacyGameSettings);
addPropertyChangedListener(onInvalidating(this::invalidate));
addPropertyChangedListener(arg -> invalidate());
}
/// Returns a debug string containing the game directory path and display metadata.
@@ -156,7 +156,14 @@ final class JsonSettingFile<T extends ObservableSetting & JsonSchemaSetting> {
///
/// @param value the settings object to observe
void installAutoSave(T value) {
value.addListener(source -> save(value));
value.addListener(observable -> {
if (value.shouldSaveImmediately(observable)) {
save(value);
value.setSavePending(false);
} else {
value.setSavePending(true);
}
});
}
/// Saves a settings object.
@@ -480,17 +480,19 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Whether UI animations are disabled.
@SerializedName("animationDisabled")
private final BooleanProperty animationDisabled = new SimpleBooleanProperty(
FXUtils.REDUCED_MOTION == Boolean.TRUE
|| !JavaRuntime.CURRENT_JIT_ENABLED
|| !FXUtils.GPU_ACCELERATION_ENABLED
);
private final ObjectProperty<@Nullable Boolean> animationDisabled = new SimpleObjectProperty<>();
/// Returns the UI animation disable property.
public BooleanProperty animationDisabledProperty() {
public ObjectProperty<@Nullable Boolean> animationDisabledProperty() {
return animationDisabled;
}
public boolean isAnimationDisabled() {
// Avoid accessing FXUtils too early during startup
return Objects.requireNonNullElseGet(animationDisabled.get(), () ->
FXUtils.REDUCED_MOTION == Boolean.TRUE || !JavaRuntime.CURRENT_JIT_ENABLED || !FXUtils.GPU_ACCELERATION_ENABLED);
}
// Networks
/// Whether HMCL automatically selects the number of download threads.
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
@@ -54,6 +55,15 @@ public final class LauncherState extends ObservableSetting implements JsonSchema
register();
}
/// Returns whether the changed field should trigger automatic persistence immediately.
@Override
public boolean shouldSaveImmediately(Observable observable) {
return observable != x
&& observable != y
&& observable != width
&& observable != height;
}
/// The schema used by this launcher state file.
@SerializedName(JsonSchema.PROPERTY_SCHEMA)
private final ObjectProperty<JsonSchema> schema = new SimpleObjectProperty<>(CURRENT_SCHEMA);
@@ -55,6 +55,20 @@ public final class SettingsManager {
private SettingsManager() {
}
/// Saves deferred launcher state changes and shutdown file saver.
public static void shutdown() {
savePendingChanges();
FileSaver.shutdown();
}
/// Saves deferred launcher state changes.
public static void savePendingChanges() {
if (launcherState != null && launcherState.isSavable() && launcherState.isSavePending()) {
launcherState.setSavePending(false);
STATE_FILE.save(launcherState);
}
}
/// The local directory storing per-workspace configuration files.
private static final Path LOCAL_CONFIG_FILES_DIRECTORY = Metadata.HMCL_LOCAL_HOME.resolve("config");
@@ -28,19 +28,12 @@ import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.theme.ResolvedTheme;
import org.jackhuang.hmcl.theme.ThemeColor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
@@ -67,23 +60,8 @@ public final class StyleSheets {
});
}
private static String toStyleSheetUri(String styleSheet, String fallback) {
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
else
try {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
Files.writeString(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
return fallback;
}
private static String toStyleSheetUri(String styleSheet) {
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
}
private static String getFontStyleSheet() {
@@ -133,7 +111,7 @@ public final class StyleSheets {
builder.append('}');
return toStyleSheetUri(builder.toString(), defaultCss);
return toStyleSheetUri(builder.toString());
}
private static String getBrightnessStyleSheet() {
@@ -184,7 +162,7 @@ public final class StyleSheets {
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
builder.append("}\n");
return toStyleSheetUri(builder.toString(), blueCss);
return toStyleSheetUri(builder.toString());
}
public static void init(Scene scene) {
@@ -47,7 +47,6 @@ public enum VersionIconType {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGE -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case LEGACY_FABRIC -> VersionIconType.LEGACY_FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
case CLEANROOM -> VersionIconType.CLEANROOM;
@@ -36,9 +36,7 @@ import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
@@ -73,6 +71,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -97,12 +96,12 @@ public final class Controllers {
public static final int MIN_CONTENT_HEIGHT = 450 + 2 + 40; // bg height + border width*2 + toolbar height
public static final int MIN_WIDTH = MIN_CONTENT_WIDTH + CUSTOM_DECORATION_SHADOW_EXTENT;
public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT;
public static final Screen SCREEN = Screen.getPrimary();
public static final Rectangle2D PRIMARY_SCREEN_BOUNDS = Screen.getPrimary().getBounds();
private static InvalidationListener stageSizeChangeListener;
private static DoubleProperty stageX = new SimpleDoubleProperty();
private static DoubleProperty stageY = new SimpleDoubleProperty();
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
private static final DoubleProperty contentX = new SimpleDoubleProperty();
private static final DoubleProperty contentY = new SimpleDoubleProperty();
private static final DoubleProperty contentWidth = new SimpleDoubleProperty();
private static final DoubleProperty contentHeight = new SimpleDoubleProperty();
private static Scene scene;
private static Stage stage;
@@ -133,12 +132,16 @@ public final class Controllers {
void run() throws Exception;
}
public static Scene getScene() {
return scene;
public static @Nullable Stage getStage() {
return stage;
}
public static Stage getStage() {
return stage;
public static ReadOnlyDoubleProperty windowWidthProperty() {
return contentWidth;
}
public static ReadOnlyDoubleProperty windowHeightProperty() {
return contentHeight;
}
@FXThread
@@ -214,64 +217,8 @@ public final class Controllers {
return decorator;
}
public static void saveWindowStates() {
saveWindowBounds();
}
private static void saveWindowBounds() {
if (stageX != null) {
state().setX(toContentX(stageX.get()) / SCREEN.getBounds().getWidth());
}
if (stageY != null) {
state().setY(toContentY(stageY.get()) / SCREEN.getBounds().getHeight());
}
if (stageHeight != null) {
state().setHeight(toContentHeight(stageHeight.get()));
}
if (stageWidth != null) {
state().setWidth(toContentWidth(stageWidth.get()));
}
}
public static void onApplicationStop() {
stageSizeChangeListener = null;
saveWindowBounds();
stageX = null;
stageY = null;
stageHeight = null;
stageWidth = null;
}
private static double toContentX(double stageX) {
return stageX + CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toContentY(double stageY) {
return stageY + CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toStageX(double contentX) {
return contentX - CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toStageY(double contentY) {
return contentY - CUSTOM_DECORATION_SHADOW_SIZE;
}
private static double toContentWidth(double stageWidth) {
return Math.max(0.0, stageWidth - CUSTOM_DECORATION_SHADOW_EXTENT);
}
private static double toContentHeight(double stageHeight) {
return Math.max(0.0, stageHeight - CUSTOM_DECORATION_SHADOW_EXTENT);
}
private static double toStageWidth(double contentWidth) {
return contentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
}
private static double toStageHeight(double contentHeight) {
return contentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
}
public static void initialize(Stage stage) {
@@ -285,7 +232,7 @@ public final class Controllers {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Screen.getPrimary().getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
@@ -293,52 +240,14 @@ public final class Controllers {
Controllers.stage = stage;
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
DoubleProperty targetProperty;
switch (sourceProperty.getName()) {
case "x": {
targetProperty = stageX;
break;
}
case "y": {
targetProperty = stageY;
break;
}
case "width": {
targetProperty = stageWidth;
break;
}
case "height": {
targetProperty = stageHeight;
break;
}
default: {
targetProperty = null;
}
}
if (targetProperty != null
&& Controllers.stage != null
&& !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());
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
double initContentWidth = Math.max(MIN_CONTENT_WIDTH, state().getWidth());
double initContentHeight = Math.max(MIN_CONTENT_HEIGHT, state().getHeight());
double initWidth = toStageWidth(initContentWidth);
double initHeight = toStageHeight(initContentHeight);
double initWidth = initContentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
double initHeight = initContentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
{
double initContentX = state().getX() * SCREEN.getBounds().getWidth();
double initContentY = state().getY() * SCREEN.getBounds().getHeight();
double initContentX = state().getX() * PRIMARY_SCREEN_BOUNDS.getWidth();
double initContentY = state().getY() * PRIMARY_SCREEN_BOUNDS.getHeight();
boolean invalid = true;
double border = 20D;
@@ -355,24 +264,65 @@ public final class Controllers {
}
if (invalid) {
initContentX = (0.5D - initContentWidth / SCREEN.getBounds().getWidth() / 2)
* SCREEN.getBounds().getWidth();
initContentY = (0.5D - initContentHeight / SCREEN.getBounds().getHeight() / 2)
* SCREEN.getBounds().getHeight();
initContentX = (0.5D - initContentWidth / PRIMARY_SCREEN_BOUNDS.getWidth() / 2)
* PRIMARY_SCREEN_BOUNDS.getWidth();
initContentY = (0.5D - initContentHeight / PRIMARY_SCREEN_BOUNDS.getHeight() / 2)
* PRIMARY_SCREEN_BOUNDS.getHeight();
}
double initX = toStageX(initContentX);
double initY = toStageY(initContentY);
double initX = initContentX - CUSTOM_DECORATION_SHADOW_SIZE;
double initY = initContentY - CUSTOM_DECORATION_SHADOW_SIZE;
stage.setX(initX);
stage.setY(initY);
stageX.set(initX);
stageY.set(initY);
contentX.set(initContentX);
contentY.set(initContentY);
}
stage.setHeight(initHeight);
stage.setWidth(initWidth);
stageHeight.set(initHeight);
stageWidth.set(initWidth);
contentHeight.set(initContentHeight);
contentWidth.set(initContentWidth);
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o;
Stage currentStage = property.getBean() instanceof Stage s ? s : null;
if (currentStage == null)
return;
boolean saveState = !currentStage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|| !currentStage.isFullScreen() && !currentStage.isMaximized());
switch (property.getName()) {
case "x" -> {
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
contentX.set(value);
if (saveState)
state().setX(value / PRIMARY_SCREEN_BOUNDS.getWidth());
}
case "y" -> {
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
contentY.set(value);
if (saveState)
state().setY(value / PRIMARY_SCREEN_BOUNDS.getHeight());
}
case "width" -> {
double value = Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
contentWidth.set(value);
if (saveState)
state().setWidth(value);
}
case "height" -> {
double value = Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
contentHeight.set(value);
if (saveState)
state().setHeight(value);
}
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
stage.xProperty().addListener(weakListener);
stage.yProperty().addListener(weakListener);
stage.heightProperty().addListener(weakListener);
@@ -585,7 +535,7 @@ public final class Controllers {
/// Shows a warning that confirms backing up a read-only settings file before overwriting it.
///
/// @param text the file-specific read-only warning
/// @param text the file-specific read-only warning
/// @param overwrite the action that backs up and overwrites the file
public static void confirmBackupAndOverwrite(String text, ThrowingRunnable overwrite) {
dialog(new MessageDialogPane.Builder(
@@ -694,6 +644,22 @@ public final class Controllers {
decorator.showToast(content);
}
public static @Nullable Path showDialog(DirectoryChooser directoryChooser) {
return FileUtils.toPath(directoryChooser.showDialog(stage));
}
public static @Nullable Path showOpenDialog(FileChooser fileChooser) {
return FileUtils.toPath(fileChooser.showOpenDialog(stage));
}
public static @Nullable Path showSaveDialog(FileChooser fileChooser) {
return FileUtils.toPath(fileChooser.showSaveDialog(stage));
}
public static @Nullable List<Path> showOpenMultipleDialog(FileChooser fileChooser) {
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(stage));
}
public static void onHyperlinkAction(String href) {
if (href.startsWith("hmcl://")) {
switch (href) {
@@ -33,6 +33,7 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import kala.encdet.EncodingDetector;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
@@ -48,7 +49,6 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.*;
@@ -149,7 +149,7 @@ public class GameCrashWindow extends Stage {
String log;
try {
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
log = EncodingDetector.MODERN_WEB.readString(latestLog);
} catch (IOException e) {
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
@@ -164,6 +164,7 @@ public final class LogWindow extends Stage {
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final BooleanProperty wrapText = new SimpleBooleanProperty(true);
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXButton btnAlwaysOnTop = FXUtils.newToggleButton4(SVG.KEEP, 20);
@@ -174,7 +175,6 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.getStyleClass().add("no-horizontal-scrollbar");
listView.getProperties().put("no-smooth-scrolling", true);
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
@@ -331,11 +331,26 @@ public final class LogWindow extends Stage {
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
wrapTextProperty().addListener((obs, oldWrap, nowWrap) -> {
if (nowWrap) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
listView.getStyleClass().add("no-horizontal-scrollbar");
} else {
maxWidthProperty().unbind();
prefWidthProperty().unbind();
setMaxWidth(Region.USE_PREF_SIZE);
setPrefWidth(Region.USE_COMPUTED_SIZE);
listView.getStyleClass().remove("no-horizontal-scrollbar");
}
// only for horizontal scrollbar
listView.requestLayout();
});
}
setPadding(new Insets(2));
setWrapText(true);
wrapTextProperty().bind(control.wrapText);
setGraphic(null);
}
@@ -395,6 +410,10 @@ public final class LogWindow extends Stage {
autoScrollCheckBox.setSelected(true);
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXCheckBox wrapTextCheckBox = new JFXCheckBox(i18n("logwindow.wrap_text"));
wrapTextCheckBox.setSelected(true);
control.wrapText.bind(wrapTextCheckBox.selectedProperty());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
@@ -414,7 +433,7 @@ public final class LogWindow extends Stage {
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
hBox.getChildren().setAll(autoScrollCheckBox, wrapTextCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
control.getGameProcess().getProcess()
.onExit()
@@ -66,6 +66,7 @@ public enum SVG {
FILE_OPEN("M6 22q-0.825 0-1.4125-0.5875T4 20v-16q0-0.825 0.5875-1.4125T6 2h8l6 6v6h-2v-5H13v-5H6v16h9v2H6Zm15.95 0.375L19 19.425v2.225h-2v-5.65h5.65v2h-2.25l2.95 2.95-1.4 1.425Zm-15.95-2.375v-16 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_FILL("M4 20q-.825 0-1.4125-.5875T2 18v-12q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8v10q0 .825-.5875 1.4125T20 20H4Z"),
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"),
@@ -22,6 +22,8 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXSpinner;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -34,8 +36,6 @@ import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -44,8 +44,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
setHeading(new Label(i18n("update.changelog")));
setBody(new JFXSpinner());
@@ -59,7 +59,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
// Downgrade update, no need to display changelog
return null;
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Document document = Jsoup.parse(WebURL.toURL(url), 30 * 1000);
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
if (node == null || !"h1".equals(node.nodeName())) {
@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
@@ -151,7 +150,7 @@ public class AccountListItem extends RadioButton {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("account.skin.upload"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
return null;
}
@@ -37,7 +37,7 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean ENABLED = !settings().animationDisabledProperty().get();
private static final boolean ENABLED = !settings().isAnimationDisabled();
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
@@ -120,7 +120,7 @@ public class FileSelector extends HBox {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
@@ -131,7 +131,7 @@ public class FileSelector extends HBox {
private void openDirectoryChooser(JFXTextField customField) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
Path dir = Controllers.showDialog(chooser);
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
@@ -20,11 +20,11 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -57,7 +57,9 @@ public class LineFileChooserButton extends LineButton {
public void fire() {
super.fire();
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
Scene scene = this.getScene();
Window owner = scene != null ? scene.getWindow() : null; // TODO: Allow user to set owner stage
String windowTitle = getFileChooserTitle();
Path initialDirectory = null;
@@ -88,7 +88,7 @@ public interface TabControl {
setSelectedIndex(index);
Tab tab = getModelItem(index);
Tab<?> tab = getModelItem(index);
if (tab != null) {
setSelectedItem(tab);
}
@@ -106,7 +106,7 @@ public interface TabControl {
final int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
final Tab value = getModelItem(i);
final Tab<?> value = getModelItem(i);
if (value != null && value.equals(tab)) {
select(i);
return;
@@ -21,11 +21,12 @@ import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.*;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -67,16 +68,18 @@ public class TaskExecutorDialogPane extends BorderPane {
center.getChildren().setAll(lblTitle, taskListPane);
}
BorderPane bottom = new BorderPane();
this.setBottom(bottom);
HBox bottom = new HBox();
bottom.setAlignment(Pos.CENTER_LEFT);
bottom.setPadding(new Insets(0, 8, 8, 8));
bottom.setSpacing(8);
this.setBottom(bottom);
{
lblProgress = new Label();
bottom.setLeft(lblProgress);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
bottom.setRight(btnCancel);
bottom.getChildren().setAll(lblProgress, spacer, btnCancel);
}
setCancel(cancel);
@@ -21,13 +21,14 @@ import com.jfoenix.controls.JFXButton;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jetbrains.annotations.NotNullByDefault;
@@ -48,18 +49,23 @@ public class GameDirectoryListItemSkin extends SkinBase<GameDirectoryListItem> {
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
SVGContainer left = (skinnable.isSelected() ? SVG.FOLDER_FILL : SVG.FOLDER).createIcon(20);
left.setMouseTransparent(true);
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
root.setLeft(left);
FXUtils.onChangeAndOperate(skinnable.selectedProperty(), active -> {
skinnable.pseudoClassStateChanged(SELECTED, active);
SVG targetIcon = active ? SVG.FOLDER_FILL : SVG.FOLDER;
if (left.getIcon() != targetIcon) {
left.setIcon(targetIcon, Motion.SHORT4);
}
});
FXUtils.onClicked(getSkinnable(), () -> GameDirectoryManager.setSelectedGameDirectory(skinnable.getGameDirectory()));
Node left = SVG.FOLDER.createIcon(20);
left.setMouseTransparent(true);
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setPickOnBounds(false);
BorderPane.setAlignment(item, Pos.CENTER);
@@ -27,11 +27,14 @@ import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -112,6 +115,30 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
return new InstallersPageSkin(this);
}
/**
* Determines whether to display the extension pane.
* <p>
* This method controls the visibility of the extension pane in the UI.
* When this method returns {@code true} and the pane is visible, users can
* interact with the features inside it, which includes triggering the
* "Reset to Default Name" button handled by {@link #resetDefaultName()}.
* </p>
*
* @return {@code true} if the extension pane should be displayed; {@code false} otherwise.
*/
protected abstract boolean showExtendPane();
/**
* Resets the name to its default value.
* <p>
* This method contains the business logic for the "Reset to Default Name" button
* located within the extension pane. This logic can only be triggered by the user
* clicking the corresponding button after {@link #showExtendPane()} returns
* {@code true} and the extension pane is successfully displayed.
* </p>
*/
protected abstract void resetDefaultName();
protected static class InstallersPageSkin extends SkinBase<AbstractInstallersPage> {
/**
* Constructor for all SkinBase instances.
@@ -127,11 +154,27 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
{
HBox versionNamePane = new HBox(8);
versionNamePane.getStyleClass().add("card-non-transparent");
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
versionNamePane.setStyle("-fx-padding: 20 16 20 16");
versionNamePane.setAlignment(Pos.CENTER_LEFT);
control.txtName.setMaxWidth(300);
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
HBox.setHgrow(control.txtName, Priority.ALWAYS);
versionNamePane.getChildren().addAll(new Label(i18n("version.name")), control.txtName);
if (control.showExtendPane()) {
JFXButton clearButton = FXUtils.newToggleButton4(SVG.CLOSE);
FXUtils.installFastTooltip(clearButton, i18n("button.clear"));
clearButton.disableProperty().bind(control.txtName.textProperty().isEmpty().or(control.txtName.disableProperty()));
clearButton.setOnAction(e -> control.txtName.clear());
JFXButton resetButton = FXUtils.newToggleButton4(SVG.RESTORE);
FXUtils.installFastTooltip(resetButton, i18n("button.reset"));
resetButton.disableProperty().bind(control.txtName.disableProperty());
resetButton.setOnAction(e -> control.resetDefaultName());
versionNamePane.getChildren().addAll(clearButton, resetButton);
}
root.setTop(versionNamePane);
}
@@ -111,4 +111,13 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
@Override
public void cleanup(SettingsMap settings) {
}
@Override
protected boolean showExtendPane() {
return false;
}
@Override
protected void resetDefaultName() {
}
}
@@ -143,4 +143,15 @@ public class InstallersPage extends AbstractInstallersPage {
txtName.setText(nameBuilder.toString());
isNameModifiedByUser = false;
}
@Override
protected boolean showExtendPane() {
return true;
}
@Override
protected void resetDefaultName() {
isNameModifiedByUser = false;
setTxtNameWithLoaders();
}
}
@@ -90,7 +90,7 @@ public final class LocalModpackPage extends ModpackPage {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
controller.onEnd();
return;
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -112,7 +111,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
return;
@@ -1,95 +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.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VanillaInstallWizardProvider implements WizardProvider {
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependencyManager;
private final DownloadProvider downloadProvider;
public VanillaInstallWizardProvider(HMCLGameRepository repository) {
this.repository = repository;
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = repository.getDependency(downloadProvider);
}
@Override
public void start(SettingsMap settings) {
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
settings.put(ModpackPage.REPOSITORY, repository);
}
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
GameBuilder builder = dependencyManager.gameBuilder();
String name = (String) settings.get("name");
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
settings.asStringMap().forEach((key, value) -> {
if (!"game".equals(key) && value instanceof RemoteVersion remoteVersion)
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
})
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@Override
public Object finish(SettingsMap settings) {
settings.put("title", i18n("install.new_game.installation"));
settings.put("success_message", i18n("install.success"));
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
return finishVersionDownloadingAsync(settings);
}
@Override
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
() -> controller.onNext(new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@Override
public boolean cancel() {
return true;
}
}
@@ -47,7 +47,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
@@ -118,7 +117,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
fileChooser.setInitialFileName(name.get() + ".zip");
}
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
Path file = Controllers.showSaveDialog(fileChooser);
if (file == null) {
controller.onEnd();
return;
@@ -40,7 +40,6 @@ import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -51,16 +50,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Provides image-format detection and loaders for image formats not handled
/// directly by JavaFX.
///
/// @author Glavo
@NotNullByDefault
/**
* @author Glavo
*/
public final class ImageUtils {
// ImageLoaders
@@ -85,37 +81,38 @@ public final class ImageUtils {
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
Supplier<Image> supplier = () -> {
SVGImage image = SVGLoader.load(content, parameters);
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};
@Nullable Image image;
SVGImage image;
if (Platform.isFxApplicationThread()) {
image = supplier.get();
image = SVGLoader.load(content, parameters);
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(supplier, Schedulers.javafx()).get();
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
}
return image;
if (image == null)
throw new IOException("Failed to load SVG image");
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
@@ -201,7 +198,6 @@ public final class ImageUtils {
// ------
/// Maximum number of leading bytes inspected when selecting an image loader.
public static final int HEADER_BUFFER_SIZE = 1024;
private static final byte[] RIFF_HEADER = {'R', 'I', 'F', 'F'};
@@ -213,272 +209,12 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
/// ASCII local name of an SVG document element.
private static final String SVG_ELEMENT_NAME = "svg";
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
/// ASCII prefix of an XML comment.
private static final String XML_COMMENT_START = "<!--";
/// ASCII suffix of an XML comment.
private static final String XML_COMMENT_END = "-->";
/// ASCII prefix of an XML processing instruction.
private static final String XML_PROCESSING_INSTRUCTION_START = "<?";
/// ASCII suffix of an XML processing instruction.
private static final String XML_PROCESSING_INSTRUCTION_END = "?>";
/// Case-sensitive ASCII prefix of an XML document type declaration.
private static final String XML_DOCTYPE_START = "<!DOCTYPE";
/// Returns whether the supplied prefix identifies an SVG XML document.
///
/// An optional UTF-8 byte-order mark, XML whitespace, comments, processing
/// instructions, and a document type declaration may precede the document
/// element. The method identifies the document element only; it does not
/// validate the remainder of the XML document. XML markup must use its
/// ASCII byte representation; UTF-16 and UTF-32 prefixes are not recognized.
///
/// @param headerBuffer leading bytes of the candidate image
/// @return whether the first XML document element is `svg`
// This is currently a simple check, more complex checks can be considered in the future
public static boolean isSVG(byte[] headerBuffer) {
int offset = hasUtf8ByteOrderMark(headerBuffer) ? 3 : 0;
while (true) {
offset = skipXmlWhitespace(headerBuffer, offset);
if (isSvgElementStart(headerBuffer, offset))
return true;
if (matchesAscii(headerBuffer, offset, XML_COMMENT_START)) {
offset = indexAfterAscii(
headerBuffer,
offset + XML_COMMENT_START.length(),
XML_COMMENT_END
);
} else if (matchesAscii(headerBuffer, offset, XML_PROCESSING_INSTRUCTION_START)) {
offset = indexAfterAscii(
headerBuffer,
offset + XML_PROCESSING_INSTRUCTION_START.length(),
XML_PROCESSING_INSTRUCTION_END
);
} else if (isDoctypeStart(headerBuffer, offset)) {
offset = skipDoctype(headerBuffer, offset);
} else {
return false;
}
if (offset < 0)
return false;
}
}
/// Returns whether the byte array starts with a UTF-8 byte-order mark.
///
/// @param data bytes to inspect
/// @return whether `data` begins with `EF BB BF`
private static boolean hasUtf8ByteOrderMark(byte[] data) {
return svgScanLimit(data) >= 3
&& data[0] == (byte) 0xef
&& data[1] == (byte) 0xbb
&& data[2] == (byte) 0xbf;
}
/// Returns the bounded length available to SVG detection.
///
/// @param data source bytes
/// @return the lesser of the array length and [#HEADER_BUFFER_SIZE]
private static int svgScanLimit(byte[] data) {
return Math.min(data.length, HEADER_BUFFER_SIZE);
}
/// Skips XML whitespace from an array offset.
///
/// @param data source bytes
/// @param start inclusive offset at which to begin
/// @return the first offset that does not contain XML whitespace
private static int skipXmlWhitespace(byte[] data, int start) {
int limit = svgScanLimit(data);
while (start < limit && isXmlWhitespace(data[start]))
start++;
return start;
}
/// Returns whether an SVG document-element qualified name begins at an array offset.
///
/// @param data source bytes
/// @param start offset of a possible element opening delimiter
/// @return whether the unprefixed or prefixed element has the local name `svg`
private static boolean isSvgElementStart(byte[] data, int start) {
int limit = svgScanLimit(data);
if (start < 0 || start >= limit || data[start] != '<')
return false;
int nameStart = start + 1;
int localNameStart = nameStart;
boolean hasPrefix = false;
for (int index = nameStart; index < limit; index++) {
byte value = data[index];
if (value == '>' || isXmlWhitespace(value)
|| value == '/' && index + 1 < limit && data[index + 1] == '>') {
return index - localNameStart == SVG_ELEMENT_NAME.length()
&& matchesAscii(data, localNameStart, SVG_ELEMENT_NAME);
}
if (value == ':') {
if (hasPrefix || index == nameStart)
return false;
hasPrefix = true;
localNameStart = index + 1;
continue;
}
boolean nameStartCharacter = index == nameStart || index == localNameStart;
if (nameStartCharacter
? !isAsciiXmlNameStart(value)
: !isAsciiXmlNamePart(value)) {
return false;
}
}
return false;
}
/// Returns whether a byte can begin an ASCII XML name component.
///
/// @param value byte to inspect
/// @return whether the byte is an ASCII letter or underscore
private static boolean isAsciiXmlNameStart(byte value) {
return value >= 'A' && value <= 'Z'
|| value >= 'a' && value <= 'z'
|| value == '_';
}
/// Returns whether a byte can continue an ASCII XML name component.
///
/// @param value byte to inspect
/// @return whether the byte is accepted after the first name character
private static boolean isAsciiXmlNamePart(byte value) {
return isAsciiXmlNameStart(value)
|| value >= '0' && value <= '9'
|| value == '-'
|| value == '.';
}
/// Returns whether an XML document type declaration begins at an offset.
///
/// @param data source bytes
/// @param start offset of a possible declaration
/// @return whether the case-sensitive keyword is followed by XML whitespace
private static boolean isDoctypeStart(byte[] data, int start) {
int keywordEnd = start + XML_DOCTYPE_START.length();
return matchesAscii(data, start, XML_DOCTYPE_START)
&& keywordEnd < svgScanLimit(data)
&& isXmlWhitespace(data[keywordEnd]);
}
/// Skips an XML document type declaration, including its internal subset.
///
/// Quoted values, comments, and processing instructions are ignored while
/// locating the declaration-closing `>`.
///
/// @param data source bytes
/// @param start offset of the `<!DOCTYPE` prefix
/// @return the offset after the declaration, or `-1` if it is incomplete
private static int skipDoctype(byte[] data, int start) {
int internalSubsetDepth = 0;
byte quote = 0;
int limit = svgScanLimit(data);
for (int index = start + XML_DOCTYPE_START.length(); index < limit; index++) {
byte value = data[index];
if (quote != 0) {
if (value == quote)
quote = 0;
continue;
}
if (value == '\'' || value == '"') {
quote = value;
continue;
}
if (matchesAscii(data, index, XML_COMMENT_START)) {
int commentEnd = indexAfterAscii(
data,
index + XML_COMMENT_START.length(),
XML_COMMENT_END
);
if (commentEnd < 0)
return -1;
index = commentEnd - 1;
continue;
}
if (matchesAscii(data, index, XML_PROCESSING_INSTRUCTION_START)) {
int instructionEnd = indexAfterAscii(
data,
index + XML_PROCESSING_INSTRUCTION_START.length(),
XML_PROCESSING_INSTRUCTION_END
);
if (instructionEnd < 0)
return -1;
index = instructionEnd - 1;
continue;
}
if (value == '[') {
internalSubsetDepth++;
} else if (value == ']' && internalSubsetDepth > 0) {
internalSubsetDepth--;
} else if (value == '>' && internalSubsetDepth == 0) {
return index + 1;
}
}
return -1;
}
/// Finds an ASCII sequence in a byte array.
///
/// @param data source bytes
/// @param start inclusive offset at which to begin searching
/// @param asciiValue ASCII sequence to find
/// @return the offset after the first match, or `-1` if no match exists
private static int indexAfterAscii(byte[] data, int start, String asciiValue) {
int lastStart = svgScanLimit(data) - asciiValue.length();
for (int index = start; index <= lastStart; index++) {
if (matchesAscii(data, index, asciiValue))
return index + asciiValue.length();
}
return -1;
}
/// Compares an ASCII string with bytes at an array offset.
///
/// @param data source bytes
/// @param start offset at which to compare
/// @param asciiValue ASCII sequence to compare
/// @return whether the complete sequence matches
private static boolean matchesAscii(byte[] data, int start, String asciiValue) {
if (start < 0 || start > svgScanLimit(data) - asciiValue.length())
return false;
for (int index = 0; index < asciiValue.length(); index++) {
if (Byte.toUnsignedInt(data[start + index]) != asciiValue.charAt(index))
return false;
}
return true;
}
/// Returns whether a byte is XML whitespace.
///
/// @param value byte to inspect
/// @return whether the byte is space, tab, carriage return, or line feed
private static boolean isXmlWhitespace(byte value) {
return value == ' ' || value == '\t' || value == '\r' || value == '\n';
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
}
private static final byte[] PNG_HEADER = {
@@ -513,7 +249,7 @@ public final class ImageUtils {
ByteBuffer buffer = ByteBuffer.wrap(headerBuffer, 8, headerBuffer.length - 8);
@Nullable PngChunkHeader header;
PngChunkHeader header;
while ((header = PngChunkHeader.readHeader(buffer)) != null) {
// https://wiki.mozilla.org/APNG_Specification#Structure
// To be recognized as an APNG, an `acTL` chunk must appear in the stream before any `IDAT` chunks.
@@ -700,7 +436,7 @@ public final class ImageUtils {
}
}
@Nullable PngAnimationControl animationControl = sequence.getAnimationControl();
PngAnimationControl animationControl = sequence.getAnimationControl();
int cycleCount;
if (animationControl != null) {
cycleCount = animationControl.numPlays();
@@ -729,7 +465,6 @@ public final class ImageUtils {
return argb;
}
/// Prevents instantiation.
private ImageUtils() {
}
}
@@ -115,7 +115,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
chooser.setTitle(i18n("settings.game.java_directory.choose"));
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) {
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
@@ -44,12 +44,7 @@ import javafx.stage.FileChooser;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorStyle;
import org.glavo.uuid.UUIDs;
import org.jackhuang.hmcl.setting.BackgroundType;
import org.jackhuang.hmcl.setting.FontManager;
import org.jackhuang.hmcl.setting.LauncherSettings;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.ThemeColorType;
import org.jackhuang.hmcl.setting.UserSettings;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
import org.jackhuang.hmcl.theme.BuiltinBackground;
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
@@ -347,20 +342,25 @@ public class PersonalizationPage extends StackPane {
String defaultPackId = "com.example.hmcl.theme-pack." + UUIDs.toCompactString(UUIDs.generateV7(exportTimestamp));
String defaultPackName = LocalDateTime.ofInstant(exportTimestamp, ZoneId.systemDefault()).format(EXPORTED_THEME_NAME_FORMATTER);
String userName = System.getProperty("user.name").trim();
String defaultAuthorName = StringUtils.isBlank(userName) ? "Unknown" : userName;
String accountName = null;
{
var currentAccount = Accounts.getSelectedAccount();
if (currentAccount != null) accountName = currentAccount.getProfileName();
}
if (StringUtils.isBlank(accountName)) accountName = System.getProperty("user.name").trim();
String defaultAuthorName = StringUtils.isBlank(accountName) ? "Unknown" : accountName;
PromptDialogPane.Builder.StringQuestion packNameQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.name"),
"")
defaultPackName)
.setPromptText(defaultPackName);
PromptDialogPane.Builder.StringQuestion versionQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.version"),
"")
ThemePackManager.CURRENT_THEME_PACK_VERSION)
.setPromptText(ThemePackManager.CURRENT_THEME_PACK_VERSION);
PromptDialogPane.Builder.StringQuestion authorNameQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.author"),
"")
defaultAuthorName)
.setPromptText(defaultAuthorName);
Controllers.prompt(new PromptDialogPane.Builder(i18n("theme_pack.export.title"), (questions, handler) -> handler.resolve())
@@ -387,7 +387,7 @@ public class PersonalizationPage extends StackPane {
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
@Nullable Path output = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
@Nullable Path output = Controllers.showSaveDialog(chooser);
if (output == null) {
return;
}
@@ -1007,6 +1007,7 @@ public class PersonalizationPage extends StackPane {
Label textOpacity = new Label();
FXUtils.setLimitWidth(textOpacity, 50);
textOpacity.setAlignment(Pos.CENTER);
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
textOpacity.textProperty().bind(valueBinding);
@@ -1171,7 +1172,8 @@ public class PersonalizationPage extends StackPane {
LineToggleButton animationButton = new LineToggleButton();
appearanceList.getContent().add(animationButton);
animationButton.selectedProperty().bindBidirectional(settings().animationDisabledProperty());
animationButton.setSelected(settings().isAnimationDisabled());
FXUtils.onChange(animationButton.selectedProperty(), value -> settings().animationDisabledProperty().set(value));
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
animationButton.setSubtitle(i18n("settings.take_effect_after_restart"));
@@ -22,8 +22,11 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -32,6 +35,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
@@ -41,7 +45,6 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.*;
import org.jackhuang.hmcl.ui.*;
@@ -159,7 +162,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
@Nullable Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
@Nullable Path file = Controllers.showOpenDialog(chooser);
if (file == null) {
return;
}
@@ -415,8 +418,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
/// @param themePack the installed theme pack to display
private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.InstalledThemePack themePack) {
ThemePackManifest manifest = themePack.manifest();
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
HBox heading = new HBox(8);
heading.setAlignment(Pos.CENTER_LEFT);
@@ -481,7 +483,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setPrefViewportWidth(520);
scrollPane.setPrefViewportHeight(Math.min(360, manifest.themes().size() * 86));
scrollPane.maxHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.55));
scrollPane.maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.55));
StackPane body = new StackPane(scrollPane);
body.setPadding(new Insets(10, 0, 0, 0));
@@ -508,6 +510,9 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
/// Toolbar shown during normal browsing.
private final HBox toolbarNormal = new HBox();
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
/// Search input.
private final JFXTextField searchField = new JFXTextField();
@@ -545,13 +550,20 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
pause.setOnFinished(event ->
skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText())));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
pause.setRate(1);
pause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> {
changeToolbar(toolbarNormal);
searchField.clear();
pause.stop();
skinnable.filteredList.setPredicate(null);
isSearching.set(false);
});
onEscPressed(searchField, closeSearchBar::fire);
@@ -586,6 +598,15 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
listView.getStyleClass().add("no-horizontal-scrollbar");
ignoreEvent(listView, KeyEvent.KEY_PRESSED, event -> event.getCode() == KeyCode.ESCAPE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label();
placeholderLabel.textProperty().bind(
Bindings.when(isSearching).then(i18n("search.no_results_found")).otherwise("")
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
center.setContent(listView);
root.getContent().add(center);
}
@@ -597,6 +618,8 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
if (newToolbar == searchBar) {
runInFX(searchField::requestFocus);
isSearching.set(true);
}
}
}
@@ -29,7 +29,7 @@ import java.nio.file.Path;
/// @author Glavo
public enum NBTFileType {
COMPRESSED("dat", "dat_old") {
COMPRESSED("nbt", "dat", "dat_old") {
@Override
public Tag read(Path file) throws IOException {
return NBTCodec.of().readTag(file);
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.terracotta;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Binding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
@@ -56,6 +57,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@@ -84,15 +86,22 @@ public class TerracottaControllerPage extends StackPane {
});
}
@SuppressWarnings("FieldCanBeLocal")
private final Binding<Boolean> focusedBinding;
@SuppressWarnings("FieldCanBeLocal")
private final WeakListenerHolder holder = new WeakListenerHolder();
/* FIXME: It's sucked to have such a long logic, containing UI for all states defined in TerracottaState, with unclear control flows.
Consider moving UI into multiple files for each state respectively. */
public TerracottaControllerPage() {
holder.add(FXUtils.observeWeak(() -> {
focusedBinding = BindingMapping.of(this.sceneProperty())
.flatMap(it -> it != null ? it.windowProperty() : new SimpleObjectProperty<>())
.flatMap(it -> it != null ? it.focusedProperty() : null, () -> false);
FXUtils.onChange(focusedBinding, focused -> {
// Run daemon process only if HMCL is focused and is displaying current node.
TerracottaManager.switchDaemon(getScene() != null && Controllers.getStage().isFocused());
}, this.sceneProperty(), Controllers.getStage().focusedProperty()));
TerracottaManager.switchDaemon(getScene() != null && focused);
});
TransitionPane transition = new TransitionPane();
@@ -91,8 +91,7 @@ public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.Da
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
if (res != null) {
installMultiDataPack(res);
}
@@ -25,6 +25,7 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -33,6 +34,7 @@ import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
@@ -51,7 +53,9 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
@@ -68,6 +72,7 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final TransitionPane toolbarPane;
@@ -79,10 +84,15 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final JFXListView<DataPackInfoObject> listView;
private final FilteredList<DataPackInfoObject> filteredList;
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
private final JFXTextField searchField;
/// Timer for debouncing search input to avoid executing search on every keystroke.
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
final Consumer<Integer> toggleSelect;
@@ -148,16 +158,21 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
searchPause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
isSearching.set(false);
searchField.clear();
searchPause.stop();
filteredList.setPredicate(null);
isSearching.set(false);
});
FXUtils.onEscPressed(searchField, closeSearchBar::fire);
searchBar.getChildren().addAll(searchField, closeSearchBar);
@@ -193,6 +208,23 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("datapack.empty"));
placeholderLabel.textProperty().bind(
Bindings.createStringBinding(() -> {
if (isSearching.get()) {
return i18n("search.no_results_found");
} else {
return i18n("datapack.empty");
}
},
isSearching)
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
this.listView.setItems(filteredList);
listView.getItems().addListener(listener);
@@ -519,7 +519,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else if (!getSkinnable().isLoading() && getSkinnable().pageCount.get() >= 0 && getSkinnable().items.isEmpty()) {
return i18n("download.failed.no_results_found");
return i18n("search.no_results_found");
} else {
return null;
}
@@ -46,8 +46,6 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -171,7 +169,7 @@ public class DownloadPage extends Control implements DecoratorPage {
fileChooser.setTitle(i18n("button.save_as"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension));
fileChooser.setInitialFileName(file.file().filename());
Path dest = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
Path dest = Controllers.showSaveDialog(fileChooser);
if (dest == null) {
return;
}
@@ -472,6 +470,9 @@ public class DownloadPage extends Control implements DecoratorPage {
case QUILT:
content.addTag(i18n("install.installer.quilt"));
break;
case LEGACY_FABRIC:
content.addTag(i18n("install.installer.legacyfabric"));
break;
}
}
@@ -557,8 +558,8 @@ public class DownloadPage extends Control implements DecoratorPage {
this.setActions(downloadButton, saveAsButton, cancelButton);
}
this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7));
this.prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
this.prefHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
onEscPressed(this, cancelButton::fire);
}
@@ -22,7 +22,8 @@ import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
@@ -51,9 +52,9 @@ import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem;
import org.jackhuang.hmcl.ui.directory.GameDirectoryPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -103,10 +104,8 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
}
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
.addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Versions::addNewGame)
.addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack)
.addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 + 12 * 2);
setLeft(pane, bottomLeftCornerList);
}
@@ -143,7 +142,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
GameDirectoryManager.registerVersionsListener(this::loadVersions);
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
setOnFailedAction(e -> Versions.addNewGame());
}
@FXThread
@@ -235,7 +234,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
searchBar.getChildren().setAll(searchField, closeSearchBar);
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("install.new_game"), SVG.DOWNLOAD, Versions::addNewGame), createToolbarButton2(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)));
toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE);
@@ -137,7 +137,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
public void installOffline() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe"));
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path file = Controllers.showOpenDialog(chooser);
if (file != null) doInstallOffline(file);
}
@@ -17,17 +17,15 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.mod.ModManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.task.Schedulers;
@@ -52,8 +50,6 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.GameInstanceLoadable, PageAware {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private final ReentrantLock lock = new ReentrantLock();
private ModManager modManager;
@@ -92,14 +88,16 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
Version resolved = repository.getResolvedPreservingPatchesVersion(instanceId);
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
modded.set(analyzer.hasModLoader());
loadMods(repository.getModManager(instanceId));
}
private void loadMods(ModManager modManager) {
setLoading(true);
if (this.modManager != modManager) {
getItems().clear();
}
this.modManager = modManager;
CompletableFuture.supplyAsync(() -> {
lock.lock();
@@ -112,6 +110,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
lock.unlock();
}
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
if (this.modManager != modManager) {
return;
}
updateSupportedLoaders(modManager);
if (exception == null) {
@@ -172,8 +174,8 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("mods.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod"));
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
if (res == null) return;
@@ -236,6 +238,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void checkUpdates(Collection<LocalModFile> mods) {
Objects.requireNonNull(mods);
if (isLoading()) {
return;
}
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = repository.getGameVersion(instanceId);
@@ -278,18 +284,6 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
}
public boolean isModded() {
return modded.get();
}
public BooleanProperty moddedProperty() {
return modded;
}
public void setModded(boolean modded) {
this.modded.set(modded);
}
public GameDirectory getGameDirectory() {
return this.repository.getGameDirectory();
}
@@ -21,8 +21,10 @@ import com.jfoenix.controls.*;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
@@ -35,12 +37,12 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.addon.*;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
@@ -61,6 +63,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
@@ -80,6 +83,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
final class ModListPageSkin extends SkinBase<ModListPage> {
private final TransitionPane toolbarPane;
@@ -88,10 +92,14 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private final HBox toolbarSelecting;
private final JFXListView<ModInfoObject> listView;
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final JFXTextField searchField;
@FXThread
private boolean isSearching = false;
/// Timer for debouncing search input to avoid executing search on every keystroke.
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
@@ -101,6 +109,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
pane.getChildren().setAll(root);
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
listView.getStyleClass().add("no-horizontal-scrollbar");
@@ -118,19 +127,22 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> search());
searchPause.setOnFinished(e -> search());
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
changeToolbar(toolbarNormal);
isSearching = false;
searchField.clear();
searchPause.stop();
isSearching.set(false);
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
});
@@ -192,7 +204,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
selectedItem -> {
if (selectedItem == null)
changeToolbar(isSearching ? searchBar : toolbarNormal);
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
else
changeToolbar(toolbarSelecting);
});
@@ -219,9 +231,26 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
listView.setCellFactory(x -> new ModInfoListCell(listView));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("mods.empty"));
placeholderLabel.textProperty().bind(
Bindings.createStringBinding(() -> {
if (isSearching.get()) {
return i18n("search.no_results_found");
} else {
return i18n("mods.empty");
}
},
isSearching)
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
skinnable.getItems().addListener((ListChangeListener<? super ModInfoObject>) c -> {
if (isSearching) {
if (isSearching.get()) {
search();
}
});
@@ -242,14 +271,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
root.getContent().add(center);
}
Label label = new Label(i18n("mods.not_modded"));
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
if (modded) pane.getChildren().setAll(root);
else pane.getChildren().setAll(label);
});
getChildren().setAll(pane);
}
@@ -264,7 +285,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}
private void search() {
isSearching = true;
isSearching.set(true);
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
@@ -379,11 +400,14 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
ModInfoDialog(ModInfoObject modInfo) {
HBox titleContainer = new HBox();
titleContainer.setSpacing(8);
titleContainer.setPadding(new Insets(0, 0, 12, 0));
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
DoubleBinding widthBinding = Controllers.windowWidthProperty().multiply(0.7);
prefWidthProperty().bind(widthBinding);
maxWidthProperty().bind(widthBinding);
var imageContainer = new ImageContainer(40);
titleContainer.setAlignment(Pos.CENTER_LEFT);
modInfo.loadIcon(imageContainer, null);
TwoLineListItem title = new TwoLineListItem();
@@ -392,13 +416,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
else
title.setTitle(modInfo.getModInfo().getName());
StringJoiner subtitle = new StringJoiner(" | ");
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
StringJoiner subtitle = new StringJoiner("\n");
subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile()));
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
subtitle.add(modInfo.getModInfo().getGameVersion());
subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
subtitle.add(modInfo.getModInfo().getVersion());
subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
@@ -418,7 +442,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
description.heightProperty().addListener((obs, oldVal, newVal) -> {
double maxHeight = stage.getHeight() * 0.5;
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
descriptionPane.setPrefViewportHeight(targetHeight);
});
@@ -35,7 +35,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.addon.*;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
@@ -57,7 +56,7 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -176,7 +175,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("resourcepack.add"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
if (files != null && !files.isEmpty()) {
addFiles(files);
}
@@ -257,6 +256,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
}
}
@NotNullByDefault
private static final class ResourcePackListPageSkin extends SkinBase<ResourcePackListPage> {
private final JFXListView<ResourcePackInfoObject> listView;
private final JFXTextField searchField = new JFXTextField();
@@ -266,7 +266,11 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
private final HBox toolbarNormal = new HBox();
private final HBox toolbarSelecting = new HBox();
private boolean isSearching;
/// Whether the search mechanism is currently active.
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
/// Timer for debouncing search input to avoid executing search on every keystroke.
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
private ResourcePackListPageSkin(ResourcePackListPage control) {
super(control);
@@ -309,19 +313,22 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
searchBar.setPadding(new Insets(0, 5, 0, 5));
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> search());
searchPause.setOnFinished(e -> search());
FXUtils.onChange(searchField.textProperty(), newValue -> {
pause.setRate(1);
pause.playFromStart();
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
searchPause.setRate(1);
searchPause.playFromStart();
}
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
changeToolbar(toolbarNormal);
isSearching = false;
searchField.clear();
searchPause.stop();
isSearching.set(false);
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
});
@@ -346,7 +353,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
selectedItem -> {
if (selectedItem == null)
changeToolbar(isSearching ? searchBar : toolbarNormal);
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
else
changeToolbar(toolbarSelecting);
});
@@ -371,6 +378,23 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
listView.setCellFactory(x -> new ResourcePackListCell(listView, control));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("resourcepack.empty"));
placeholderLabel.textProperty().bind(
Bindings.createStringBinding(() -> {
if (isSearching.get()) {
return i18n("search.no_results_found");
} else {
return i18n("resourcepack.empty");
}
},
isSearching)
);
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
Bindings.bindContent(listView.getItems(), control.getItems());
listView.setOnContextMenuRequested(event -> {
@@ -403,7 +427,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
}
private void search() {
isSearching = true;
isSearching.set(true);
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
@@ -567,8 +591,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
HBox titleContainer = new HBox();
titleContainer.setSpacing(8);
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
ImageContainer imageContainer = new ImageContainer(40);
imageContainer.setImage(packInfoObject.getIcon());
@@ -594,7 +617,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
FXUtils.onChange(description.heightProperty(), newVal -> {
double maxHeight = stage.getHeight() * 0.5;
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
descriptionPane.setPrefViewportHeight(targetHeight);
});
@@ -149,7 +149,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
fileChooser.setTitle(i18n("schematics.add.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
i18n("extension.schematic"), "*.litematic"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
if (files != null && !files.isEmpty()) {
addFiles(files);
}
@@ -636,6 +636,12 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
private final class SchematicsPageSkin extends ToolbarListPageSkin<Item, SchematicsPage> {
SchematicsPageSkin() {
super(SchematicsPage.this);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("schematics.empty"));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
}
@Override
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Path;
@@ -76,7 +75,7 @@ public class VersionIconDialog extends DialogPane {
private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(FXUtils.getImageExtensionFilter());
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
Path selectedFile = Controllers.showOpenDialog(chooser);
if (selectedFile != null) {
try {
repository.setVersionIconFile(versionId, selectedFile);
@@ -17,11 +17,10 @@
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
@@ -29,8 +28,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.QuickPlayOption;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
@@ -92,6 +93,7 @@ public final class Versions {
}
Controllers.taskDialog(
new FileDownloadTask(downloadURLs, modpack)
.setName(file.name())
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
ModpackInstallWizardProvider installWizardProvider;
@@ -263,7 +265,7 @@ public final class Versions {
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
Path file = Controllers.showSaveDialog(chooser);
if (file != null) {
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
String defaultExt = getDefaultScriptExtension();
@@ -24,6 +24,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
@@ -162,6 +163,12 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
WorldBackupsPageSkin() {
super(WorldBackupsPage.this);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("world.backup.empty"));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
}
@Override
@@ -639,7 +639,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
fileChooser.setInitialFileName("icon.png");
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
Path iconPath = Controllers.showOpenDialog(fileChooser);
if (iconPath == null) return;
Image image;
@@ -27,6 +27,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
@@ -140,7 +141,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
if (res == null || res.isEmpty()) return;
installWorld(res.get(0));
@@ -216,6 +217,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
WorldListPageSkin() {
super(WorldListPage.this);
StackPane placeholderContainer = new StackPane();
placeholderContainer.getStyleClass().add("notice-pane");
Label placeholderLabel = new Label(i18n("world.empty"));
placeholderContainer.getChildren().add(placeholderLabel);
listView.setPlaceholder(placeholderContainer);
}
@Override
@@ -74,7 +74,7 @@ public final class WorldManageUIUtils {
fileChooser.setTitle(i18n("world.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
fileChooser.setInitialFileName(world.getWorldName() + ".zip");
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
Path file = Controllers.showSaveDialog(fileChooser);
if (file == null) {
return;
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
@@ -43,10 +44,10 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -127,23 +128,21 @@ public final class UpdateHandler {
throw new IOException("Current JAR is not verified");
}
CompletableFuture<Void> future = new CompletableFuture<>();
var latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
Controllers.saveWindowStates();
SettingsManager.savePendingChanges();
} finally {
future.complete(null);
latch.countDown();
}
});
try {
future.get();
} catch (ExecutionException | InterruptedException ignored) {
latch.await();
} catch (InterruptedException ignored) {
// Ignore
}
try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
@@ -222,13 +221,35 @@ public final class UpdateHandler {
commandline.add("-jar");
commandline.add(jar.toAbsolutePath().toString());
commandline.addAll(Arrays.asList(appArgs));
LOG.info("Starting process: " + commandline);
LOG.info("Starting process: " + maskCommandline(commandline));
new ProcessBuilder(commandline)
.directory(Paths.get("").toAbsolutePath().toFile())
.inheritIO()
.start();
}
private static String maskCommandline(List<String> commandline) {
return commandline.stream().map(str -> {
if (str.startsWith("-D")) {
int eqIdx = str.indexOf('=');
if (eqIdx != -1) {
String key = str.substring(2, eqIdx);
String value = str.substring(eqIdx + 1);
if (key.contains("http.proxy") ||
key.startsWith("https.proxy") ||
key.startsWith("socksProxy") ||
key.equals("hmcl.microsoft.auth.id") ||
key.equals("hmcl.curseforge.apikey")
) {
return "-D" + key + "=" + (value.isEmpty() ? "" : value.charAt(0) + "*".repeat(value.length() - 1));
}
}
}
return str;
}).collect(Collectors.joining(" "));
}
private static Optional<Path> tryRename(Path path, String newVersion) {
String filename = path.getFileName().toString();
Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename);
@@ -21,6 +21,7 @@ import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.ui.CrashWindow;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -101,7 +102,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.error("Unable to handle uncaught exception", handlingException);
}
FileSaver.shutdown();
SettingsManager.shutdown();
LOG.shutdown();
}
}
@@ -399,7 +399,6 @@ download.external_link=Visit Download Website
download.failed=Failed to download "%1$s", response code: %2$d.
download.failed.empty=No versions are available. Please click here to go back.
download.failed.no_code=Failed to download
download.failed.no_results_found=No results found
download.failed.refresh=Failed to fetch version list. Please click here to retry.
download.game=New Game
download.provider.bmclapi=BMCLAPI & MCIM
@@ -545,9 +544,6 @@ game.crash.reason.modmixin_failure=The game crashed because some mods failed to
This generally means that the mod has a bug or is incompatible with the current environment.\n\
\n\
You can check the log to find the error mod.
game.crash.reason.mod_repeat_installation=The game crashed because of duplicate mods.\n\
\n\
Each mod can only be installed once. Please delete the duplicate mod and then relaunch the game.
game.crash.reason.forge_error=Forge/NeoForge may have provided error information.\n\
\n\
You can view the log and make corresponding processing according to the log information in the error report.\n\
@@ -948,6 +944,7 @@ logwindow.terminate_game=Kill Game Process
logwindow.title=Log
logwindow.help=You can navigate to the HMCL community and find others for help.
logwindow.autoscroll=Auto-scroll
logwindow.wrap_text=Wrap Text
logwindow.export_game_crash_logs=Export Crash Logs
logwindow.export_dump=Export Game Stack Dump
logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help.
@@ -1133,14 +1130,14 @@ modrinth.category.vanilla-like=Vanilla-like
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Worldgen
modrinth.category.8x-=8x-
modrinth.category.8x-=8x or lower
modrinth.category.16x=16x
modrinth.category.32x=32x
modrinth.category.48x=48x
modrinth.category.64x=64x
modrinth.category.128x=128x
modrinth.category.256x=256x
modrinth.category.512x+=512x+
modrinth.category.512x+=512x or higher
mods=Mods
mods.add=Add
@@ -1151,6 +1148,7 @@ mods.disable=Disable
mods.download=Download Mod
mods.download.title=Download Mod - %1s
mods.download.recommend=Recommended Mod Version - Minecraft %1s
mods.empty=No Mods
mods.enable=Enable
mods.game.version=Game Version
mods.manage=Mods
@@ -1184,6 +1182,7 @@ nbt.title=View File - %s
datapack=Data Packs
datapack.add=Add
datapack.add.title=Choose data pack archive you want to add
datapack.empty=No Data Packs
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
datapack.title=World [%s] - Data Packs
@@ -1202,6 +1201,7 @@ world.backup.create.new_one=New Backup
world.backup.create.failed=Failed to create backup.\n%s
world.backup.create.success=Successfully created a new backup: %s
world.backup.delete=Delete this backup
world.backup.empty=No Backup
world.backup.processing=Backing up ...
world.chunkbase=Chunk Base
world.chunkbase.end_city=End City
@@ -1221,6 +1221,7 @@ world.delete=Delete the World
world.delete.failed=Failed to delete world.\n%s
world.download=Download
world.download.title=Download World - %1s
world.empty=No Worlds
world.export=Export the World
world.export.title=Choose the directory for this exported world
world.export.location=Save As
@@ -1295,6 +1296,7 @@ resourcepack.add.title=Choose resource pack archive you want to add
resourcepack.delete.failed=Failed to delete resource pack
resourcepack.download=Download
resourcepack.download.title=Download Resource Pack - %1s
resourcepack.empty=No Resource Packs
resourcepack.manage=Resource Packs
resourcepack.update_in_modpack.warning=Updating resource packs in a modpack can lead to irreparable results, possibly leading to incorrect textures and sounds. Are you sure you want to update?
resourcepack.warning.invalid=Invalid pack meta
@@ -1317,6 +1319,7 @@ schematics.create_directory.failed=Failed to create directory
schematics.create_directory.failed.already_exists=Directory already exists
schematics.create_directory.failed.empty_name=Name cannot be empty
schematics.create_directory.failed.invalid_name=Name contains invalid characters
schematics.empty=No Schematics
schematics.info.description=Description
schematics.info.enclosing_size=Enclosing Size
schematics.info.name=Name
@@ -1338,6 +1341,7 @@ search.sort=Sort By
search.first_page=First
search.previous_page=Previous
search.next_page=Next
search.no_results_found=No results found
search.last_page=Last
search.page_n=%1$d / %2$s
@@ -407,7 +407,7 @@ extension.command=سكريبت شيل macOS
extension.datapack=أرشيف حزمة البيانات
extension.mod=ملف مود
extension.modloader=مثبّت محمّل المودات
extension.modloader.installer=مثبّت محمّل المودات
extension.resourcepack=أرشيف حزمة الموارد
extension.schematic=ملف مخطط
extension.world=أرشيف العالم
@@ -508,9 +508,6 @@ game.crash.reason.modmixin_failure=تعطّلت اللعبة لأن بعض ال
يعني هذا عموماً أن المود يحتوي على خطأ أو غير متوافق مع البيئة الحالية.\n\
\n\
يمكنك مراجعة السجل للعثور على المود المسبب للخطأ.
game.crash.reason.mod_repeat_installation=تعطّلت اللعبة بسبب مودات مكررة.\n\
\n\
يمكن تثبيت كل مود مرة واحدة فقط. يُرجى حذف المود المكرر ثم إعادة تشغيل اللعبة.
game.crash.reason.forge_error=قد يكون Forge/NeoForge قد أعطى معلومات عن الخطأ.\n\
\n\
يمكنك مراجعة السجل ومعالجة المشكلة وفقاً لمعلومات السجل في تقرير الخطأ.\n\
@@ -110,7 +110,6 @@ account.methods.microsoft.purchase=Comprar Minecraft
account.methods.microsoft.snapshot.website=Sitio web oficial
account.methods.offline=Sin conexión
account.methods.offline.name.special_characters=Utilice solo letras, números y guiones bajos (máximo 16 caracteres)
account.methods.offline.name.invalid.tip=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.
account.methods.offline.name.invalid=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.\n\
\n\
\ · Algunos nombres de usuario legítimos: HuangYu, huang_Yu, Huang_Yu_123;\n\
@@ -577,9 +576,6 @@ game.crash.reason.modmixin_failure=El juego se ha colgado debido a que algunos m
Esto generalmente significa que el mod tiene un error o no es compatible con el entorno actual. \n\
\n\
Puede consultar el registro para ver si hay un mod incorrecto.
game.crash.reason.mod_repeat_installation=El juego se ha colgado debido a los mods duplicados.\n\
\n\
Cada mod sólo puede instalarse una vez. Elimine el mod duplicado y vuelva a ejecutar el juego.
game.crash.reason.forge_error=Forge/NeoForge puede haber proporcionado información errónea.\n\
\n\
Puede ver el registro y realizar las acciones correspondientes de acuerdo con la información de registro en el informe de errores.\n\
@@ -1037,14 +1033,8 @@ modrinth.category.vanilla-like=Similar a la vainilla
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Generación de mundos
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
modrinth.category.48x=48x
modrinth.category.64x=64x
modrinth.category.128x=128x
modrinth.category.256x=256x
modrinth.category.512x+=512x+
modrinth.category.8x-=8x o más bajo
modrinth.category.512x+=512x o mayor
mods=Mods
mods.add.failed=No se ha podido añadir el mod %s.
@@ -363,7 +363,6 @@ game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
game.crash.reason.mod_repeat_installation=現在のゲームには複数の同一の Mod が繰り返しインストールされており、各 Mod は 1 回しか表示できません。ゲームを開始する前に繰り返しの Mod を削除してください。
game.crash.reason.forge_error=Forge がエラー メッセージを表示した可能性があります。 \nログを表示し、エラー レポートのログ情報に従って対応するアクションを実行できます。 \nエラー メッセージが表示されない場合は、エラー レポートをチェックして、エラーがどのように発生したかを知ることができます。\n%1$s
game.crash.reason.mod_resolution0=mod の問題により、現在のゲームを続行できません。 \n間違った mod のログを確認できます。
game.crash.reason.java_version_is_too_high=Java のバージョンが高すぎて実行を継続できないため、現在のゲームがクラッシュしました。 \nゲームを起動する前に、グローバル ゲーム設定またはゲーム固有の設定の Java パス タブで Java の以前のバージョンに切り替えてください。 \nそうでない場合は、<a href="https://www.java.com/download/">java.com (Java8)</a> または <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java17)</a> およびその他のプラットフォームをダウンロードしてインストールします (インストール後にランチャーを再起動します)。
@@ -613,7 +612,6 @@ modpack.files.resourcepacks=リソース/テクスチャパック
modpack.files.saves=保存されたマップ
modpack.files.scripts=MineTweaker構成
modpack.files.servers_dat=サーバーリスト
modpack.install=%s modpackをインストールします
modpack.installing=modpackのインストール
modpack.introduction= Curse, Modrinth, MultiMC, MCBBS Modpackがサポートされています。
modpack.invalid=無効なmodpackファイル。
@@ -674,6 +672,8 @@ modrinth.category.utility=ユーティリティ
modrinth.category.worldgen=Worldgen
modrinth.category.datapack=Datapack
modrinth.category.folia=Folia
modrinth.category.8x-=8x以下
modrinth.category.512x+=512x以上
mods=Mods
mods.add.failed=mods %s の追加に失敗しました。
@@ -903,7 +903,6 @@ world.add.invalid=無可辨此緊囊
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=方備……
@@ -111,7 +111,6 @@ account.methods.microsoft.purchase=Купить Minecraft
account.methods.microsoft.snapshot.website=Официальный сайт
account.methods.offline=Офлайн
account.methods.offline.name.special_characters=Использовать только буквы, цифры и подчеркивания (максимум 16 символов)
account.methods.offline.name.invalid.tip=Для имени пользователя рекомендуется использовать только английские буквы, цифры и подчеркивания, а его длина не должна превышать 16 символов.
account.methods.offline.name.invalid=Для имени пользователя рекомендуется использовать только английские буквы, цифры и подчеркивания, а его длина не должна превышать 16 символов.\n\
\n\
\ · Законный: HuangYu, huang_Yu, Huang_Yu_123;\n\
@@ -473,9 +472,6 @@ game.crash.reason.modmixin_failure=Игра отказала из-за нево
Обычно это означает, что мод содержит ошибку или несовместим с текущей средой.\n\
\n\
Вы можете проверить журнал на наличие неправильного мода.
game.crash.reason.mod_repeat_installation=Игра отказала из-за дублирования модов.\n\
\n\
Каждый мод может быть установлен только один раз. Пожалуйста, удалите дублирующий мод, а затем заново запустите игру.
game.crash.reason.need_jdk11=Игра отказала из-за неподходящей версии Java.\n\
\n\
Вам необходимо загрузить и установить Java 11, а также указать ее в разделе «Глобальные настройки / Раздельные настройки для сборки → Java».
@@ -1030,14 +1026,8 @@ modrinth.category.vanilla-like=Ванильное
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Генерация мира
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
modrinth.category.48x=48x
modrinth.category.64x=64x
modrinth.category.128x=128x
modrinth.category.256x=256x
modrinth.category.512x+=512x+
modrinth.category.8x-=8x и ниже
modrinth.category.512x+=512x и выше
mods=Моды
mods.add.failed=Не удалось установить мод %s.
@@ -447,8 +447,6 @@ game.crash.reason.duplicated_mod=Гра не може продовжувати
game.crash.reason.modmixin_failure=Гра зазнала збою, оскільки деякі моди не вдалося ввести. \n\
Це, як правило, означає, що мод має помилку або несумісний з поточним середовищем. \n\
Ви можете перевірити журнал, щоб знайти помилковий мод.
game.crash.reason.mod_repeat_installation=Гра зазнала збою через дублікати модів. \n\
Кожен мод можна встановити лише один раз. Видаліть дублікат мода та знову запустіть гру.
game.crash.reason.forge_error=Forge/NeoForge може надати інформацію про помилку. \n\
Ви можете переглянути журнал та відповідно обробити його відповідно до інформації про помилку в звіті. \n\
Якщо ви не бачите повідомлення про помилку, ви можете переглянути звіт про помилку, щоб зрозуміти, як сталася помилка. \n\
@@ -978,14 +976,14 @@ modrinth.category.vanilla-like=Схожий на ваніль
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Генерація світу
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
modrinth.category.48x=48x
modrinth.category.64x=64x
modrinth.category.128x=128x
modrinth.category.256x=256x
modrinth.category.512x+=512x+
modrinth.category.8x-=×8 і менше
modrinth.category.16x=×16
modrinth.category.32x=×32
modrinth.category.48x=×48
modrinth.category.64x=×64
modrinth.category.128x=×128
modrinth.category.256x=×256
modrinth.category.512x+=×512 і більше
mods=Моди
mods.add.failed=Не вдалося додати мод %s.
@@ -334,7 +334,7 @@ curse.category.9049=精緻儲存 (Refined Storage)
curse.category.10754=農夫樂事 (Farmer's Delight)
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
curse.category.5244=字體包
curse.category.5244=字體包
curse.category.5193=封包
curse.category.399=蒸汽朋克
curse.category.396=128x
@@ -397,7 +397,6 @@ download.external_link=開啟下載網站
download.failed=下載失敗:%1$s\n錯誤碼:%2$d
download.failed.empty=沒有可供安裝的版本,按一下此處返回。
download.failed.no_code=下載失敗
download.failed.no_results_found=搜尋無結果
download.failed.refresh=載入版本清單失敗,按一下此處重試。
download.game=新遊戲
download.provider.bmclapi=BMCLAPI & MCIM
@@ -494,7 +493,6 @@ game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
game.crash.reason.mod_repeat_installation=目前遊戲由於重複安裝了多個相同的模組,無法繼續執行。\n每個模組只能出現一次,請刪除重複的模組,然後再啟動遊戲。
game.crash.reason.forge_error=Forge/NeoForge 可能已經提供了錯誤資訊。\n你可以查看日誌,並根據錯誤報告中的日誌資訊進行對應處。\n如果沒有看到報錯資訊,可以查看錯誤報告了解錯誤具體是如何發生的。\n%1$s
game.crash.reason.mod_resolution0=目前遊戲由於一些模組出現問題,無法繼續執行。\n你可以查看日誌尋找出錯模組。
game.crash.reason.need_jdk11=目前遊戲由於 Java 版本不合適,無法繼續執行。\n你需要下載安裝 Java 11,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 11.x 的版本。
@@ -755,6 +753,7 @@ logwindow.terminate_game=結束遊戲處理程式
logwindow.title=日誌
logwindow.help=你可以前往 HMCL 社群,向他人尋求幫助
logwindow.autoscroll=自動滾動
logwindow.wrap_text=自動換行
logwindow.export_game_crash_logs=匯出遊戲崩潰資訊
logwindow.export_dump=匯出遊戲執行堆疊
logwindow.export_dump.no_dependency=你的 Java 不包含用於建立遊戲執行堆疊的相依元件。請前往 Discord 或 HMCL QQ 群尋求幫助。
@@ -938,6 +937,14 @@ modrinth.category.vanilla-like=類原生
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=世界生成
modrinth.category.8x-=8× 以下
modrinth.category.16x=16×
modrinth.category.32x=32×
modrinth.category.48x=48×
modrinth.category.64x=64×
modrinth.category.128x=128×
modrinth.category.256x=256×
modrinth.category.512x+=512× 以上
mods=模組
mods.add=新增模組
@@ -948,6 +955,7 @@ mods.disable=停用
mods.download=模組下載
mods.download.title=模組下載 - %1s
mods.download.recommend=推薦版本 - Minecraft %1s
mods.empty=沒有模組
mods.enable=啟用
mods.game.version=遊戲版本
mods.manage=模組管理
@@ -981,6 +989,7 @@ nbt.title=查看檔案 - %s
datapack=資料包
datapack.add=新增資料包
datapack.add.title=選取要新增的資料包壓縮檔
datapack.empty=沒有資料包
datapack.reload.toast=Minecraft 正在執行,請使用 /reload 指令重新載入資料包
datapack.title=世界 [%s] - 資料包
@@ -999,6 +1008,7 @@ world.backup.create.new_one=建立新備份
world.backup.create.failed=建立備份失敗。\n%s
world.backup.create.success=成功建立新備份:%s
world.backup.delete=刪除此備份
world.backup.empty=沒有備份
world.backup.processing=正在備份中……
world.chunkbase=世界地圖
world.chunkbase.end_city=終界城地圖
@@ -1018,6 +1028,7 @@ world.delete=刪除此世界
world.delete.failed=刪除世界失敗。\n%s
world.download=下載世界
world.download.title=世界下載 - %1s
world.empty=沒有世界
world.export=匯出此世界
world.export.title=選取該世界的儲存位置
world.export.location=儲存到
@@ -1088,6 +1099,7 @@ resourcepack.add.title=選取要新增的資源包壓縮檔
resourcepack.delete.failed=刪除資源包失敗
resourcepack.download=下載資源包
resourcepack.download.title=資源包下載 - %1s
resourcepack.empty=沒有資源包
resourcepack.manage=資源包管理
resourcepack.update_in_modpack.warning=更新模組包中的資源包可能導致模組包損壞,使遊戲紋理和音效異常。你確定要更新嗎?
resourcepack.warning.invalid=資源包中繼資料無效
@@ -1110,6 +1122,7 @@ schematics.create_directory.failed=建立目錄失敗
schematics.create_directory.failed.already_exists=目錄已存在
schematics.create_directory.failed.empty_name=名稱不能為空
schematics.create_directory.failed.invalid_name=名稱中包含無效字元
schematics.empty=沒有投影檔
schematics.info.description=描述
schematics.info.enclosing_size=框大小
schematics.info.name=名稱
@@ -1131,6 +1144,7 @@ search.sort=排序
search.first_page=第一頁
search.previous_page=上一頁
search.next_page=下一頁
search.no_results_found=搜尋無結果
search.last_page=最後一頁
search.page_n=%d / %s
@@ -399,7 +399,6 @@ download.external_link=打开下载网站
download.failed=下载失败: %1$s,\n错误码:%2$d\n你可以点击右上角帮助按钮进行求助。
download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你可以点击右上角帮助按钮进行求助)
download.failed.no_code=下载失败
download.failed.no_results_found=搜索无结果
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
download.game=新游戏
download.provider.bmclapi=BMCLAPI & MCIM
@@ -760,6 +759,7 @@ logwindow.terminate_game=结束游戏进程
logwindow.title=日志
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
logwindow.autoscroll=自动滚动
logwindow.wrap_text=自动换行
logwindow.export_game_crash_logs=导出游戏崩溃信息
logwindow.export_dump=导出游戏运行栈
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 寻求帮助。
@@ -943,6 +943,8 @@ modrinth.category.vanilla-like=类原生
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=世界生成
modrinth.category.8x-=8x 或更低
modrinth.category.512x+=512x 或更高
mods=模组
mods.add=添加模组
@@ -953,6 +955,7 @@ mods.disable=禁用
mods.download=模组下载
mods.download.title=模组下载 - %1s
mods.download.recommend=推荐版本 - Minecraft %1s
mods.empty=没有模组
mods.enable=启用
mods.game.version=游戏版本
mods.manage=模组管理
@@ -986,6 +989,7 @@ nbt.title=查看文件 - %s
datapack=数据包
datapack.add=添加数据包
datapack.add.title=选择要添加的数据包压缩包
datapack.empty=没有数据包
datapack.reload.toast=Minecraft 正在运行,请使用 /reload 命令重新加载数据包
datapack.title=世界 [%s] - 数据包
@@ -1004,6 +1008,7 @@ world.backup.create.new_one=创建新备份
world.backup.create.failed=创建备份失败。\n%s
world.backup.create.success=成功创建新备份:%s
world.backup.delete=删除此备份
world.backup.empty=没有备份
world.backup.processing=正在备份中……
world.chunkbase=世界地图
world.chunkbase.end_city=末地城地图
@@ -1023,6 +1028,7 @@ world.delete=删除此世界
world.delete.failed=删除世界失败。\n%s
world.download=下载世界
world.download.title=世界下载 - %1s
world.empty=没有世界
world.export=导出此世界
world.export.title=选择该世界的存储位置
world.export.location=保存到
@@ -1093,6 +1099,7 @@ resourcepack.add.title=选择要添加的资源包压缩包
resourcepack.delete.failed=删除资源包失败
resourcepack.download=下载资源包
resourcepack.download.title=资源包下载 - %1s
resourcepack.empty=没有资源包
resourcepack.manage=资源包管理
resourcepack.update_in_modpack.warning=更新整合包中的资源包可能导致整合包损坏,使游戏纹理和音效异常。你确定要更新吗?
resourcepack.warning.invalid=资源包元数据无效
@@ -1115,6 +1122,7 @@ schematics.create_directory.failed=创建文件夹失败
schematics.create_directory.failed.already_exists=文件夹已存在
schematics.create_directory.failed.empty_name=名称不能为空
schematics.create_directory.failed.invalid_name=名称中包含非法字符
schematics.empty=没有原理图
schematics.info.description=描述
schematics.info.enclosing_size=包围盒尺寸
schematics.info.name=名称
@@ -1136,6 +1144,7 @@ search.sort=排序
search.first_page=第一页
search.previous_page=上一页
search.next_page=下一页
search.no_results_found=搜索无结果
search.last_page=最后一页
search.page_n=%d / %s
File diff suppressed because it is too large. Load diff
+36 -140
View File
@@ -152,7 +152,7 @@ scientists-dream;145;;Scientific Dreams;;
abandend;146;;AbandEnd;;
tech-expansion;147;;Tech Expansion;;
minefanstasyworld;148;;我的幻想世界;MineFantasyWorld;
slightly-vanilla-flavoured;149;;Slightly Flavoured / Slightly Vanilla Flavoured;;
slightly-vanilla-flavoured;149;;Slightly Vanilla Flavoured;;
all-the-mods;150;;All the Mods;;ATM1
crackpack-3;151;;Crackpack 3;;
valhelsia-3;152;;Valhelsia 3;;
@@ -213,7 +213,7 @@ better-minecraft-plus;207;;Better Minecraft [PLUS];;
parasites-by-forge-labs;208;;Parasites by Forge Labs;;
create-live;209;;Create Live;;
airaka-tech;211;;艾瑞卡科技;Airaka Tech;
;212;;TeaCon整合包;TeaCon;
;212;;TeaCon;;
originssmp;213;;起源SMP;OriginsSMP;
create-live-2;214;;Create Live 2;;
origin-smp-copy;215;;Origin - SMP;;
@@ -252,7 +252,7 @@ better-minecraft-otv-edition;249;;Better Minecraft (OfflineTV Edition);;
evolution-reset;250;;进化:重启;Evolution: Reset;ER
the-nuclear-wasteland;251;;核荒原;The Nuclear Wasteland;
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BoC
;253;;边界生存:存世;Border Survival;BSV
;253;;边界生存;Border Survival;BSV
medieval-mc-forge-mmc1;255;;Medieval Minecraft;;MMC
heavens-of-sorcery;256;;巫术天堂;Heavens of Sorcery;
herodotus;257;;希罗多德;Herodotus-Authentic Edition:Official;HDS
@@ -498,7 +498,7 @@ other-body;504;;Other Body;;
;506;;莱姆星云;Lime Nebula;LN
void-genesis;507;;Void Genesis;;
create-flavored;508;;Create Flavored;;
terrafirmagreg-vintage;509;;群峦格雷;TerraFirmaGreg;TFG
terrafirmagreg-vintage;509;;TerraFirmaGreg;;TFG
create-astral;510;;机械动力:星辰;Create: Astral;
;511;;起源创造;CreateOrigin;CO
random-item-skyblock-recondite-i;512;;Random Item Island - Recondite I;;
@@ -516,7 +516,7 @@ pick-the-stars;523;;摘星;Pick The Stars;PTS
earth-technology;524;;Earth Technology;;
;525;;核乐不为;;ZMC
ftb-stoneblock-3;526;;FTB StoneBlock 3;;
create-mod-plus;527;;Create+;;
create-mod-plus;527;;Create +;;
multiplayer-optimization;528;;RawDiamondMC的优化包;RawDiamondMC's optimization pack;MPO
chroma-endless;529;;Chroma Endless;;
ragnamod-vi-in-the-sky;530;;Ragnamod VI In The Sky;;
@@ -548,7 +548,7 @@ gregic-skies;555;;Gregic Skies;;
;557;;齿轮颂歌:暮光;Gear Carol;
pluma;558;;Pluma, a Journey to the Future!;;Pluma
all-the-mods-7-to-the-sky;559;;All the Mods 7 - To the Sky;;ATM7S
;560;;月见苍穹传说;TsukiCupolaCraft;TSCC
;560;;月见苍穹传说;TskimiCanopyCraft;TSCC
;561;;最后的战役:劫后余生;The Last Stand:Aftermath;LSA/TLS
;562;;小猪配平;Peppa Pig's Burning Broom;2P2B
zerblands-remastered;563;;Zerblands-Remastered;;
@@ -672,7 +672,7 @@ a-new-technological-era;681;;A New Technological Era;;ANTE
musketeer;683;;Musketeer - One for All;;
;684;;匠造之传;The Legend of Tinker;TLT
;685;;我的骑士:怪物大乱斗;;
;686;;塔可努的原生优化整合包;TNF-Vanilla / TechNoob's Vanilla Boost Pack;TNVBP
;686;;塔可努的原生优化整合包;TechNoob's Vanilla Boost Pack;TNVBP
;687;;灵茗屿空岛;LMY SKY;
;688;;红石生电优化;Redstone Survival Optimization;RSO
;689;;恐惧深渊;Abyss of Fear;AoF
@@ -734,7 +734,7 @@ life-in-the-village-3;740;;Life in the village 3;;LitV3
cook-in-skyblock;746;;天空厨房;Cook in skyland;CiS
;747;;被恐惧支配 1;Dominated by Fear 1;DoF
new-vanilla-newhome;748;;新的原版;New Vanilla;NVA
tfru;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
stoneopolis;750;;Stoneopolis;;
ftb-skies-expert;751;;FTB Skies Expert;;
wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
@@ -806,6 +806,7 @@ la;818;;胶兽大冒险;Latex Advanture;LA
mystics-monstrosity;819;;Mystic's Monstrosity;;
;820;;更完美的MC;More Perfect Minecraft;MPMC
;821;;动画世界:烂柯人;AnimateCraft: Lankeren;
;822;;New Create;;NC
land-of-memories;823;;回忆之地;;LOM
;824;;异世界冒险2;;
ragnamod-vii;825;;Ragnamod VII;;
@@ -865,7 +866,7 @@ create-vanilla-exploration;879;;Vanilla Create Exploration;;
progress;880;;Progress: Create;;
aeon-automata;881;;Aeon Automata;;AA
;882;;被恐惧支配2:收容失效;Dominated by Fear 2 Containment Breach;DoF2
;883;;剑拔弩张之时;;TC
tensioncraft;883;;剑拔弩张之时;;TC
project-architect-2;884;;Project Architect 2;;
;885;;接入铁路网络:我的世界铁路;MTR Railway Network Access;RNA-MTR
create-mekanized;886;;Create: Mekanized;;
@@ -922,23 +923,24 @@ setgamedifficulty-reika;936;;SetGameDifficulty Reika;;
epic-journey-gtceu;937;;Epic Journey GTCEu;;
terrafirmacraft-leap-the-top;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
gregtech-skybound;939;;Gregtech: Skybound;;
simple-tfc;940;;简单群峦;Simple TFC;SPTFC
simple-tfc;940;;简单群峦;Simple TFC;
ftb-presents-direwolf20-s14;941;;FTB Presents Direwolf20 1.21;;
final-judgment-save-the-world;942;;Final Judgment:Save the world;;
;943;;极端世界:重置;Extreme World Reset;EWR
explore-everything;944;;空岛:探索;Explore Everything;SEE
epic-journey-gtceu-conquerors-edition;945;;Epic Journey GTCEu - Conqueror's Edition;;
the-undead-blooms;946;;亡灵绽放 | 真菌孢子启示录 |;The Undead Blooms | Fungal Spore Apocalypse |;
;947;;星地征程;Starland Journey re;SJRE
;948;;希望与绝望-惊变100天;Hope and Despair;HAD
;947;;星地征程;Starland Journey;SJ
;948;;希望与绝望-惊变100天;Hope and Despair;had
;949;;梦篡光阴;Dreams Distort Overture;DDO
;950;;最后的光芒;The Last Light;TLL
endil-expert-edition;951;;Endil's Expert Edition;;EEE
peace-of-mind;952;;Peace of Mind;;
distantly-optimized;953;;Distantly Optimized;;DO
farming-experience;954;;Farming Experience;;
hungry-for-tech-2;955;;渴望科技2;Hungry For Tech 2;
hungry-for-tech-2;955;;Hungry For Tech 2;;
remain;957;;Remain;;
;958;;末日狂徒;;
gregtania6-skyblock;959;;Gregtania6 Skyblock;;
harmonious-engineering;960;;Harmonious Engineering;;
valley-of-farming-and-ranching;961;;农牧物语;Valley of Farming and Ranching;
@@ -983,7 +985,7 @@ industrial-machines;1000;;Industrial Machines;;
auto-terrafirmacraft-tfc-create;1001;;Auto-TerraFirmaCraft (TFC + Create);;ATFC
;1002;;时间之书:星的日记;The Book of Time: A Star's Journal;TASL
;1003;;特摄世界;Tokusatsu World;TW
;1004;;转世之旅;Tensura: Journey of Reincarnation;
journey-of-reincarnation;1004;;转世之旅;Tensura: Journey of Reincarnation;
;1005;;冒险与休闲;Adventure and Leisure;AL
enigmatica10;1006;;Enigmatica 10;;E10
mc-chocolate-edition;1007;;Chocolate Edition Minecraft;;
@@ -1000,7 +1002,7 @@ kumo-o-tagayasu;1017;;耕云钓月;Kumo O Tagayasu;KOTS
;1018;;无限战争4;Infinity War4;INF4
;1019;;群峦传说:工业长路;;
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
asig;1021;;浅忆基础整合;Asai Integrated Modpack;
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
create-star-valley;1022;;Create: Gears of History;;
;1023;;阿尔法计划;AlphaPlan;AP
spell-dimension;1024;;咒次元;Spell Dimension;SD
@@ -1013,7 +1015,7 @@ terminal;1030;;终末;Terminal;
isolated-crystal-3;1031;;异次元水晶 3;Isolated Crystal 3;
;1032;;机械动力:无限总是小于零;Create:Infinity Always Smaller Than Zero;CIASZ
;1033;;踪灭I-起源;;
;1034;;踪灭II;Endtime II;
;1034;;踪灭II;;
rabbits-dawn-craft;1035;;某兔子的破晓之界;A Certain Rabbit's DawnCraft - Dawn of the Deep Soul;RDC
;1036;;铁砧天空;;ASKY
grand-architect-journey;1037;;伟大工程巡礼;Grand Architecture Journey;GAJ
@@ -1073,7 +1075,7 @@ roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
egged-up;1094;;Egged Up;;
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;VEFC
the-fool;1096;;愚者;The Fool;TF
;1096;;愚者;The Fool;TF
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
@@ -1117,11 +1119,11 @@ life-in-the-village-4;1136;;Life in the village 4;;LITV4
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
;1138;;奥术咖啡馆;Arcane Cofferia;
unyielding;1139;;Unyielding;;
;1140;;剑与王国;;sword_and_kingdom
;1140;;剑与王国;;SNK
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
mc-eternal-2;1142;;永恒的MC 2;MC Eternal 2;MCE2
solo-leveling-reawakening;1143;;Solo Leveling - Reawakening;;
create-ultimate-selection;1144;;Create: Ultimate Selection;;
create-ultimate-selection;1144;;Create: Ultimate Selection - QUESTS NOW!;;
;1145;;元素觉醒;Elemental Awakening;
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
;1147;;锻造之旅;;
@@ -1143,13 +1145,13 @@ blue-archive-x-mineraft;1163;;我的世界x蔚蓝档案;BlueArchive x Minecraft;
;1164;;Fresh & Smooth;;
;1165;;机动重启工程:风之旅;The Caravan of Winds;TCW
;1166;;Furry Fandom Club Boss Rush Official Offline;;FFCBR
;1167;;极致沉浸;Immersive Ultra;
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
;1168;;Polymania!;;
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
beloong;1170;;化龍;BeLoong;
;1171;;畅游自然;;SiN
;1171;;畅游自然;;SiF
;1172;;退休工艺;Retirement Craft;RmC
;1173;;溯光暗潮;TRACING LIGHT · DARK TIDE;NRG
;1173;;溯光暗潮:云影;NS's Random Gunfight;NRG
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
;1175;;硬核惊变;Hardcore Surprise;HS
society-sunlit-valley;1176;;Society: Sunlit Valley;;
@@ -1177,7 +1179,7 @@ repalmon;1198;;Re帕鲁梦;RePalmon;RPLM
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
arcaneaxis-ars;1200;;秘法象限;ArcaneAxis;ARS
gregtech-easy;1201;;格雷科技简单版;GregTech Easy;GTE
create-exploring-the-path-of;1202;;机境交响;Creation Exploring the Road of;CETR
;1202;;机境交响;Creation Exploring the Road of;CETR
multiblockful-skies;1203;;Multiblockful Skies;;
;1204;;刀剑物语;SwordStory;SS
;1205;;无垠征途;The Unbounded Expedition;TUE
@@ -1192,13 +1194,13 @@ farmy-skyblock;1213;;Farmy Skyblock;;
bloody-skyblock-modpack;1214;;Bloody Skyblock;;
starages;1215;;Star Space Ages;;
create-spectral-cuisine;1216;;Create: Spectral Cuisine;;
project-infinity-0-1;1217;;无限计划;Project Infinity 0.1;
project-infinity-0-1;1217;;Project Infinity 0.1;;
create-live-6;1218;;Create Live 6;;
;1219;;全面包围-硬核惊变;Full encirclement;FLET
;1220;;科波菲尔;Copperfield;CPF
;1221;;试用核心/I.O.P的水槽基础整合;Trial Essential;
tnp-limitless-8;1222;;TNP Limitless 8;;
mechanomania;1223;;重度机械症;Mechanomania;
;1223;;重度机械症;Mechanomania;
;1224;;寻此苦旅3;Per Aspera3;PA3
calamity-rpg-iii-wounds-of-the-world;1225;;Calamity RPG III : Wounds of the World;;
;1226;;烛枫公会优化整合包;;ZFGH
@@ -1219,17 +1221,17 @@ erstwhile-journey-2;1240;;往昔之旅2;Erstwhile Journey 2;EJ2
;1241;;诡厄:使徒;;GRAE
;1242;;时间之书 : 星之旅行-重制版;BookofTime StarTravel;BKSL
cook-in-skyland-elegant-feast;1243;;天空厨房:典雅盛宴;Cook in Skyland Elegant Feast;CiS2
ftb-stoneblock-4;1244;;石头世界4;FTB StoneBlock 4;SB4
ftb-stoneblock-4;1244;;FTB StoneBlock 4;;SB4
techevdiscovery;1245;;TechEv || Discovery;;
g3-project;1246;;G3 project;;
;1247;;新州;Xinzhou;XZ
;1247;;新州;Xin Zhou;XZ
emc-and-fe-plus3;1248;;EMC & FE +++;;E&F2
terracraft-confluence;1249;;泰拉工艺:汇流;TerraCraft:Confluence;TCC
;1250;;探索领域;Explore Realm;ER
beyond-cosmo;1251;;Beyond Cosmo;;
;1252;;闲居物语;Travel Serere Homestead;TSH
;1252;;闲居物语;Travel Architecture Dreams;TAD
high-altitude-alchemy;1253;;High-Altitude Alchemy;;
create-boundary-engineer;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
skyopolis-evolved;1255;;Skyopolis: Evolved;;
;1256;;IFR的1.12.2优化整合包;IFR's Forgified Optimization;IFO
;1257;;无穷匠艺;;
@@ -1260,7 +1262,7 @@ mythcrafts;1281;;MYTHCRAFT 5;;MC
;1282;;牧场物语;Harvest Moon;
;1283;;逃逸:寄生末日;SRP: Escape Parasite Doom;SRP:epd
;1284;;Ice and M;;IAM
autumn-floods;1285;;秋水;autumn floods;AF
;1285;;秋水;autumn floods;AF
;1286;;V2O5;;
;1287;;地牢冒险者;Dungeon Adventurer;DA
ultramodern-optimization;1289;;超现代优化;Ultramodern optimization;UO
@@ -1271,14 +1273,14 @@ pandemonium-nexus-anarchic-aeon;1293;;万象枢纽:无序纪元;Pandemonium Ne
;1294;;菌染亵渎;Fungal Infection Blasphemy;FIB
;1295;;Ice and M:Sky;;IAMS
;1296;;TUFF PERFORMANCE;;
;1297;;灾厄巫咒;Curlamoety;
;1297;;灾厄巫咒;;
;1298;;子弹呼啸 : 重制;Bullet Whizz : Remake;BWR
mybiomesobeautiful;1299;;My Biomes Is So Beautiful;;
create-capitalist;1300;;Create Capitalist;;
diving-beyond;1301;;Diving Beyond;;
;1302;;奇星·奥术;Mystic Star_Arcane;
guild-adventures;1303;;Guild Adventures;;
;1304;;艾斯爱慕:无限构件:现代工业;Spore and M:Infinity Mechanism:Modern Industrialization;SMIM
;1304;;艾斯爱慕:新视野;Spore and M:New Horizons;SMNH
otherworld-dnd;1305;;Otherworld [Dungeons & Dragons];;
;1306;;奇幻起源;Fantasy Origins;
projects;1307;;ProjectS;;
@@ -1316,7 +1318,7 @@ farming-crossing-6-dinner-party;1334;;Farming Crossing 6: Dinner Party!;;
;1339;;机械动力:枪火不断;Create: Gunfire Never Stops;CGNS
;1340;;逆转未来;Battle For The Future;BFF
day-of-mind;1341;;DayOfMind;;DOM
create-adventure-survival-ii;1342;;机械动力:探险生存 2;Create Adventure Survival II;
;1342;;机械动力:探险生存 2;Create Adventure Survival 2;
;1343;;云游四海;WAYFARING;WF
;1344;;阿卡迪亚的天启;The Apocalypse of Arcadia;TAOA
;1345;;纯净原版・焕新;Pure Vanilla Renewal;PVR
@@ -1325,110 +1327,4 @@ create-adventure-survival-ii;1342;;机械动力:探险生存 2;Create Adventur
dragns-o-suite-pack;1348;;DragN's O-Suite Pack;;
;1349;;机械动力:工业与牧歌;Create: Industry and Pastoral;CIAP
;1350;;灾厄纪行;;
beebeeblock-the-skyhive;1351;;蜂蜂空坊;Beebeeblock: The Skyhive;BBB
the-last-world-shines;1352;;末世生辉;The Last World Shines;TLWS
;1353;;MineOptimiz;;
journey-to-infinity;1354;;Journey To Infinity;;
;1355;;混元登仙;;HATI
flying-path;1356;;飞舞之路;Flying Path;
create-craft-quiet;1357;;机械动力:匠途闲境;Create:Craft&Quiet;ccq
sky-new-horizons;1358;;Sky New Horizons;;SkyNH
;1359;;天启之路;The Path Of Revelation;TPOR
mana-and-mechanisms;1360;;Mana & Mechanisms;;
;1361;;胶兽异界之旅Ⅱ;;
mechanical-dragon;1362;;龙之机械;Dragon create;DC
;1363;;香草精华;;VE
submerged-2;1364;;Seaopolis Submerged 2;;
;1365;;天煞孤星;A STAR DOOMED TO LONELINESS;ALONG
;1366;;全亡灵启示录;All Undead Apocalypse;AUA
;1367;;空流整合包;SkyFlow;
;1368;;飞升者的陨落;Fall of Ascension;FOA
;1369;;万华工艺录;KaleidoCraftia;KlC
;1370;;多方块笙息空岛;;DSK
etbw2-demo;1371;;探索光明的世界2;Explore the Bright World 2;EtBW2
stellarisworkshop;1372;;孤星工坊;StellarisWorkshop;GTSW
enders-journey-beyond-the-end;1373;;Ender's Journey - Beyond The End;;
mysticalblockreloaded;1374;;Mystical Block Reloaded;;
cozy-mizuno-minecraft;1375;;A Cozy Mizuno World;;
;1376;;新生异土;Tensura Neo Otherworld;
extraordinary-energy-modern;1377;;Extraordinary Energy : Modern;;
;1378;;铁砧工艺-七天创世;;
avaritia-project;1379;;Avaritia Project;;
after-the-nukes;1380;;After The Nukes;;
abyssal-ascent;1381;;Abyssal Ascent;;
desolate-planet;1382;;Desolate Planet;;
;1383;;德雷马尔:神化;Drehmal: Apotheosis;DHA
project-arc-light-the-hanging-pavilion;1384;;弧光计划:空中楼阁;Project Arc Light: The Hanging Pavilion;PAHP
;1385;;文明:创世;Civilization Isolaria;
blockitecture;1386;;方块建筑学;Blockitecture (Block Architecture);
;1388;;曙光空岛;;
;1389;;千界一梦;The Oneiropolis;TOP
;1390;;乌托星岛·创世旅途 3.0;EpicUraNepAdventure 3.0;EUNA3
itaj;1391;;It Takes a Journey;;ITAJ
taltale-pack;1392;;Taltale Pack;;
waccyys-fantasia;1393;;waccyy's Fantasia;;
all-the-mods-11;1394;;All The Mods 11;;ATM11
create-live-7;1395;;Create Live 7;;
;1396;;锻造之旅—无尽征途;;wjzt
;1397;;魔法现代化工业笙息空岛;;MXSK
nuclear-tech-integrated;1398;;核科技:集成;Nuclear Tech: Integrated;
crashed;1399;;Crashed;;
;1400;;格雷科技:万子科技;Greg Tech : Wanzi Technology;GTWT
moddedtogether;1401;;Modded Together;;
;1402;;附魔工程;;
;1403;;机械动力:航空学新视野2.0;Create Aero: New Horizons2.0;CANH
;1404;;Better with Datapack;;
restlesshorizons;1405;;Restless Horizons;;RH
;1406;;虚空行者;Void Wayfarer;VW
;1407;;模块化整合包:农业;Modular Modpack: Agriculture;MMA
the-song-of-crafting;1408;;合成颂歌;The Song of Crafting;
awakening;1409;;AMP - Awakening Classic;;
;1410;;末世再现;;CATT
;1411;;死亡突围:朝花暮拾;DeadoutRe:Duskora;DRD
craftoria-2;1412;;Craftoria 2;;
;1413;;这是一个水槽包;This is the sink pack;
;1414;;绝境中的生存;Survival in Desperation;
;1415;;等价无尽机械;PeojectE Avaritia Create;
;1416;;破灭与重生:破晓之途;Ruins and Rebirth:Path to Daybreak;RRPD
;1417;;最后那些天-低性能优化版;Those Last Day Boost;
;1418;;涅槃;Nirvana;UPN
enigmatic-skies;1419;;Enigmatic Skies;;
;1420;;Spark+ 原版红石增强优化;Spark+ Vanilla Redstone Optimization;SVRO
;1421;;混沌之地;TheChaoticnia;
;1422;;消逝的曙光;Dusk of Dawn;DOW
cook-in-skyland-recaked;1423;;天空厨房:重制版;Cook in Skyland Recaked;CiSR
rlcraft-fanmade;1424;;RLCraft Fanmade;;
unaltered;1425;;Unaltered+;;
sendims-banforges;1426;;千界万锻;Myriad Dimensional Forge;SDBF
;1427;;超级优化;Super Optimized;SO
society-sunlit-cobblemon;1428;;Society: Sunlit Cobblemon;;
create-aeronautica;1429;;Create Aeronautica;;
;1430;;群星入侵;Astrincursion;
;1431;;涟漪之篇;As Written By Whom;AWBW
starbuncles-gift;1432;;星宝石兽的礼物;starbuncles gift;
;1433;;Ember Peril: Silent Expanse;;EPSE
sky-bees-reborn;1434;;Sky Bees Reborn;;
who-am-i-not-me;1435;;我非我;Who Am I - Not Me;
all-the-hacks;1436;;All The Hacks;;ATH
;1437;;新:模拟大都市;;NSUK
;1438;;异界工匠;;
;1439;;工业升级指导版;Industrial upgrade 101;IU101
;1440;;假面骑士全明星编年史;;KRASC
;1441;;极速优化;Ultra Fast Optimization;UFO
new-age-science-and-technology;1442;;新时代科技;New Age Science and Technology;NAST
;1443;;屎生万物;EverythingFromPoop;EFP
;1444;;天顶征途;Zenith Conquest;
;1445;;瓶中伊甸;Eden in the Flask;EITF
aircrashed-engineer-ii;1446;;坠机工程师2;Aircrashed Engineer II;ACEII
star-avaritia;1447;;寰宇星光;Star Avaritia;SA
astrids-fnaf-pack;1448;;Astrid's FNAF Pack;;
hp-create-world-of-gears;1449;;HP - Create World of Gears;;
the-cursed-wanderer-reborn;1450;;咒游诸界:重生;The Cursed Wanderer: Reborn;TCWR
;1451;;机械之旅;Mechanical Create;
;1452;;探索MC小马的世界;Explore MLP World;EPW
tech101;1453;;科技入门学;Tech101;
endfield-new-horizons;1454;;终末地工业:新视野;Endfield:New Horizons;efnh
dungeon-train;1455;;地牢列车;Dungeon train;
project-arc-light-dimensional-rift;1456;;弧光计划:维度裂隙;Project Arc Light - Dimensional Rift;PA2
;1457;;恶意世界;HostileCraft;HC
adj;1458;;A Distant Journey;;ADJ
beebeeblock-the-skyhive;1351;;蜂蜂空坊;Beebeeblock: The Skyhive;BTS
-571
View File
@@ -1045,138 +1045,6 @@
}
}
},
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-arm64.jar",
"sha1": "101c753b129dbdeed565798d259b2b9544ef3878",
"size": 126343
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-arm64.jar",
"sha1": "b558d38d8eaa32831bb66835aae163428fe69646",
"size": 235928
}
}
},
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-arm64.jar",
"sha1": "f2083556a7d8a02e4abeb2b933642250ef06c61d",
"size": 866036
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-arm64.jar",
"sha1": "84f229d793388be57ae5555b0cfee09b5990fa20",
"size": 80850
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-arm64.jar",
"sha1": "e2f84903e71f7f2c5fe11a0f6de9576da270c503",
"size": 144552
}
}
},
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-arm64.jar",
"sha1": "74f1cd22ccf4fd7432b982372b8a34c93cd9aacf",
"size": 258996
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-arm64.jar",
"sha1": "97296e394d3d7dfebcc68754ab8273d44d8e9602",
"size": 45345
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-arm64.jar",
"sha1": "6babd5ddf9b4850e91a952d494cfd22b895ce200",
"size": 1300898
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-arm64.jar",
"sha1": "f17eb9b12086809a31e61734dabc1dea80f6b7b2",
"size": 3540777
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-arm64.jar",
"sha1": "85d2636eb1368a3864fb10faec02a4269020b31e",
"size": 1166740
}
}
},
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-arm64.jar",
"sha1": "0a9e20e7d8ae4d0106cd23ae58a1b72ca151c49f",
"size": 60863
}
}
},
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-arm64.jar",
"sha1": "577e41b6d0006f3d3f841c14abe19736e695a760",
"size": 1460461
}
}
},
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
"downloads": {
@@ -3088,181 +2956,6 @@
}
},
"org.lwjgl:lwjgl-sdl:3.4.2:natives-linux": null,
"org.lwjgl:lwjgl:3.4.3": {
"name": "org.lwjgl:lwjgl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
"sha1": "6105690714874b995aa7812e4f7732a21109276c",
"size": 1157957
}
}
},
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
"name": "org.glavo.hmcl:lwjgl3-natives:3.4.1-rc1",
"downloads": {
"classifiers": {
"linux-loongarch64": {
"path": "org/glavo/hmcl/lwjgl3-natives/3.4.1-rc1/lwjgl3-natives-3.4.1-rc1-linux-loongarch64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/hmcl/lwjgl3-natives/3.4.1-rc1-linux-loongarch64/lwjgl3-natives-3.4.1-rc1-linux-loongarch64.jar",
"sha1": "3fed872fc2f9929d2be25389dd312dfb48e9f89e",
"size": 22854580
}
}
},
"extract": {
"exclude": [
"META-INF/"
]
},
"natives": {
"linux": "linux-loongarch64"
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.3": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
"sha1": "16b663092854fc6adfe0a941e61cae2bb2e437b6",
"size": 47872
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-openal:3.4.3": {
"name": "org.lwjgl:lwjgl-openal:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
"sha1": "8508bd60589de0539aa465b78fcf838efd07ae9a",
"size": 153919
}
}
},
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-opengl:3.4.3": {
"name": "org.lwjgl:lwjgl-opengl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
"sha1": "830412cab823c029cda6b9729f9d2ed36cf79959",
"size": 937660
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-glfw:3.4.3": {
"name": "org.lwjgl:lwjgl-glfw:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
"sha1": "a782b1ddd175c9107bf300fd92920579ea65e2a4",
"size": 151893
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-stb:3.4.3": {
"name": "org.lwjgl:lwjgl-stb:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
"sha1": "f077c3dafc31924fbe8acb0b913f08977ba27f8e",
"size": 142927
}
}
},
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-tinyfd:3.4.3": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
"sha1": "562697d40dbd2ed0424bf24dcf65d51170f81adf",
"size": 15931
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-freetype:3.4.3": {
"name": "org.lwjgl:lwjgl-freetype:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
"sha1": "caa75a37e857efc9b0686e434d708d6420c5b6ea",
"size": 464342
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-shaderc:3.4.3": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
"size": 153051
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-spvc:3.4.3": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
"size": 148285
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-vma:3.4.3": {
"name": "org.lwjgl:lwjgl-vma:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
"size": 109209
}
}
},
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": null,
"org.lwjgl:lwjgl-vulkan:3.4.3": {
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
"size": 8356700
}
}
},
"org.lwjgl:lwjgl-sdl:3.4.3": {
"name": "org.lwjgl:lwjgl-sdl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1.jar",
"sha1": "ec28db6e949478044c649fea25eae43b1b161971",
"size": 983498
}
}
},
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": null,
"net.java.dev.jna:jna:5.8.0": {
"name": "net.java.dev.jna:jna:5.13.0",
"downloads": {
@@ -5164,138 +4857,6 @@
}
}
},
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-riscv64.jar",
"sha1": "38374c04f498a9b214f7cb28cfd3fcc9737b8065",
"size": 127129
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-riscv64.jar",
"sha1": "79b62b3efdcdba28d2f89e6a7d99d88cf3cda48f",
"size": 248363
}
}
},
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-riscv64.jar",
"sha1": "f4d3d1050f244b3a7fe9ddc02e94fbe0c507fc4d",
"size": 938685
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-riscv64.jar",
"sha1": "56ecca3a33c7966f3d678737757f5feb3efd03c2",
"size": 79430
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-riscv64.jar",
"sha1": "7f88d98db5c2fdd33063b5810df8a9809fd0983b",
"size": 141071
}
}
},
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-riscv64.jar",
"sha1": "8f0aeb06e1d532fd9411d08dcff6c0c3572d71a6",
"size": 268102
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-riscv64.jar",
"sha1": "f9b6c895de7f43caf50c12b8d30fa3aef13e82ff",
"size": 56940
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-riscv64.jar",
"sha1": "809c6df123ae9ea36aa3117a368e7d300ed712eb",
"size": 1409111
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-riscv64.jar",
"sha1": "1cb0fc92286da27d5782140a8ba72af5458eb1cf",
"size": 3702544
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-riscv64.jar",
"sha1": "38610faedc2a7c44f02c0bc7246ef1f8357a2846",
"size": 1221331
}
}
},
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-riscv64.jar",
"sha1": "e4dc73c846d42eb63c39c2b30d7cb5983a59bdbe",
"size": 66532
}
}
},
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-riscv64.jar",
"sha1": "30cd5ef9665905be2c0c7325dc848296d4788b49",
"size": 1496337
}
}
},
"com.github.oshi:oshi-core:6.6.5": {
"name": "com.github.oshi:oshi-core:6.8.0",
"downloads": {
@@ -7493,138 +7054,6 @@
}
}
},
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-freebsd.jar",
"sha1": "fbab1de5df7db20be4c74bf6cf14750f599e6d79",
"size": 110742
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-freebsd.jar",
"sha1": "30a905bf255a0eac294da13cdfddb2963bda4541",
"size": 160297
}
}
},
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-freebsd.jar",
"sha1": "6f72cdd56726eacaaa1e4f5824f9fc88f0bc2272",
"size": 851111
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-freebsd.jar",
"sha1": "97992b72536b99ad922ac1318f397e049de50223",
"size": 83069
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-freebsd.jar",
"sha1": "783d56acc7b0bc5690f4d139d843d5cc083a7ef9",
"size": 108309
}
}
},
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-freebsd.jar",
"sha1": "0422952f19f24b9a10a29fcc44ac77ed8b842b6a",
"size": 225716
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-freebsd.jar",
"sha1": "5650a5b1cddce100f5c13761f7e4a5a6cf248320",
"size": 40582
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-freebsd.jar",
"sha1": "3938689ef674b5894b41af23de67aa53cf5c6a58",
"size": 1217752
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-freebsd.jar",
"sha1": "843c2c6b1588175e28eefe260d3833244119607b",
"size": 3063282
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-freebsd.jar",
"sha1": "f66777d0f5e784c203fda965e28e6f41f6287eed",
"size": 1122077
}
}
},
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-freebsd.jar",
"sha1": "b160d451c6224139bcb47d565dac76b1d242cf2b",
"size": 63336
}
}
},
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-freebsd.jar",
"sha1": "aad4d48eb5b3b72d550589fa6d07bbc6113b442e",
"size": 1242611
}
}
},
"net.java.jinput:jinput-platform:2.0.5:natives": null,
"com.mojang:text2speech:1.10.3:natives": null,
"com.mojang:text2speech:1.11.3:natives": null,
@@ -443,25 +443,25 @@
"module": "javafx.base",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-base",
"version": "14.0.2.1-freebsd",
"version": "21.0.11-freebsd",
"classifier": "freebsd",
"sha1": "7bac900f0ab0d4d6dcf178252cf37ee1e0470d40"
"sha1": "91890866a767230184fe4f18ab9a19d19bed887d"
},
{
"module": "javafx.graphics",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-graphics",
"version": "14.0.2.1-freebsd",
"version": "21.0.11-freebsd",
"classifier": "freebsd",
"sha1": "7773ce02d1dc29160801e4e077ed2a26e93bed13"
"sha1": "bd9f01d923db548eb8ac7889d96036cd8297520f"
},
{
"module": "javafx.controls",
"groupId": "org.glavo.hmcl.openjfx",
"artifactId": "javafx-controls",
"version": "14.0.2.1-freebsd",
"version": "21.0.11-freebsd",
"classifier": "freebsd",
"sha1": "34a3dd3ccbc898bacfdcb3256cfb87ddc50621d3"
"sha1": "41590baecf9637e3fc1f7d9479527cf6eb9e8d44"
}
]
}
@@ -23,12 +23,42 @@ import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
/// Tests for detached launcher state migration.
@NotNullByDefault
public final class LauncherStateTest {
/// Tests that settings listeners receive the observable field that changed.
@Test
public void reportsChangedFieldToListeners() {
LauncherState state = new LauncherState();
AtomicBoolean notified = new AtomicBoolean();
state.addListener(source -> {
assertSame(state.xProperty(), source);
notified.set(true);
});
state.setX(0.25);
assertTrue(notified.get());
}
/// Tests that only window geometry fields defer automatic persistence.
@Test
public void identifiesDeferredWindowGeometryFields() {
LauncherState state = new LauncherState();
assertFalse(state.shouldSaveImmediately(state.xProperty()));
assertFalse(state.shouldSaveImmediately(state.yProperty()));
assertFalse(state.shouldSaveImmediately(state.widthProperty()));
assertFalse(state.shouldSaveImmediately(state.heightProperty()));
assertTrue(state.shouldSaveImmediately(state.schemaProperty()));
assertTrue(state.shouldSaveImmediately(state.promptedVersionProperty()));
assertTrue(state.shouldSaveImmediately(state.getShownTips()));
}
/// Tests extracting runtime state fields from a legacy config object.
@Test
public void extractsLauncherStateFromLegacyConfigJson() {
@@ -17,26 +17,17 @@
*/
package org.jackhuang.hmcl.ui.image;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Verifies image-format detection from bounded input prefixes.
@NotNullByDefault
public final class ImageUtilsTest {
/// Reads the image-loader detection prefix of a test resource.
///
/// @param fileName resource file name
/// @return up to [ImageUtils#HEADER_BUFFER_SIZE] leading bytes
private static byte[] readHeaderBuffer(String fileName) {
try (var input = Files.newInputStream(Path.of("src/test/resources/image/" + fileName))) {
return input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
@@ -46,15 +37,6 @@ public final class ImageUtilsTest {
}
}
/// Encodes an XML fixture without introducing non-ASCII bytes.
///
/// @param value fixture text
/// @return US-ASCII bytes
private static byte[] asciiBytes(String value) {
return value.getBytes(StandardCharsets.US_ASCII);
}
/// Verifies APNG detection distinguishes animated and static PNG files.
@Test
public void testIsApng() {
assertTrue(ImageUtils.isApng(readHeaderBuffer("16x16.apng")));
@@ -65,7 +47,6 @@ public final class ImageUtilsTest {
assertFalse(ImageUtils.isApng(readHeaderBuffer("16x16-animation-lossy.webp")));
}
/// Verifies WebP detection recognizes all supported WebP variants.
@Test
public void testIsWebP() {
assertFalse(ImageUtils.isWebP(readHeaderBuffer("16x16.apng")));
@@ -76,66 +57,4 @@ public final class ImageUtilsTest {
assertTrue(ImageUtils.isWebP(readHeaderBuffer("16x16-animation-lossy.webp")));
}
/// Verifies SVG detection accepts XML prologs and an SVG document element.
@Test
public void testIsSVG() {
assertTrue(ImageUtils.isSVG(asciiBytes("<svg/>")));
assertTrue(ImageUtils.isSVG(asciiBytes(" \t\r\n<svg viewBox='0 0 16 16'>")));
assertTrue(ImageUtils.isSVG(
"\ufeff<?xml version='1.0' encoding='UTF-8'?>\n<svg/>"
.getBytes(StandardCharsets.UTF_8)
));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<?xml version='1.0'?>"
+ "<?xml-stylesheet href='<svg.css'?>"
+ "<!-- <svg/> -->"
+ "<svg xmlns='http://www.w3.org/2000/svg'>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<!DOCTYPE svg ["
+ "<!ENTITY quoted ']> <svg>'>"
+ "<!-- ]> <svg -->"
+ "<!ELEMENT svg ANY>"
+ "]><svg/>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<!--" + "x".repeat(ImageUtils.HEADER_BUFFER_SIZE - 13) + "--><svg/>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<svg:svg xmlns:svg='http://www.w3.org/2000/svg'/>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<image:svg xmlns:image='http://www.w3.org/2000/svg'/>"
)));
}
/// Verifies SVG detection rejects incomplete, embedded, and non-SVG roots.
@Test
public void testIsNotSVG() {
assertFalse(ImageUtils.isSVG(new byte[0]));
assertFalse(ImageUtils.isSVG(asciiBytes("<svg")));
assertFalse(ImageUtils.isSVG(asciiBytes("<svg-image/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<svg:circle/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<image:svg-icon/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<SVG/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("plain text <svg/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<!-- <svg/> -->")));
assertFalse(ImageUtils.isSVG(asciiBytes(
"<?xml version='1.0'?><html><svg/></html>"
)));
assertFalse(ImageUtils.isSVG(asciiBytes(
"<!DOCTYPE svg [<!-- incomplete ]> <svg -->"
)));
assertFalse(ImageUtils.isSVG(asciiBytes(
"<!--" + "x".repeat(ImageUtils.HEADER_BUFFER_SIZE) + "--><svg/>"
)));
}
/// Verifies sniffing routes a prefixed SVG document to the SVG loader.
@Test
public void testGuessLoaderDetectsPrefixedSVG() {
assertSame(ImageUtils.SVG, ImageUtils.guessLoader(asciiBytes(
"<svg:svg xmlns:svg='http://www.w3.org/2000/svg'/>"
)));
}
}
+1 -1
View File
@@ -14,6 +14,7 @@ tasks.compileJava {
dependencies {
api(libs.kala.compress.zip)
api(libs.kala.compress.tar)
api(libs.kala.encoding.detctor)
api(libs.simple.png.javafx)
api(libs.gson)
api(libs.tomlj)
@@ -23,7 +24,6 @@ dependencies {
api(libs.constant.pool.scanner)
api(libs.nanohttpd)
api(libs.jsoup)
api(libs.chardet)
api(libs.jna)
api(libs.pci.ids)
api(libs.hello.nbt)
@@ -38,7 +38,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModManager extends LocalAddonManager<LocalModFile> {
public static final List<String> MOD_EXTENSIONS = List.of("jar", "zip", "litemod");
public static final List<String> MOD_EXTENSIONS = List.of("jar", "litemod");
@FunctionalInterface
private interface ModMetadataReader {
@@ -58,7 +58,6 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
);
map.put("jar", zipReaders);
map.put("zip", zipReaders);
map.put("litemod", List.of(pair(LiteModMetadata::fromFile, ModLoaderType.LITE_LOADER)));
READERS = map;
@@ -32,19 +32,17 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Checksum;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -186,65 +184,23 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
}
}
/// Calculates the CurseForge fingerprint without retaining the filtered file in memory.
static long calculateFingerprint(Path file) throws IOException {
try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
long startPosition = channel.position();
byte[] bufferArray = new byte[1024 * 1024];
ByteBuffer buffer = ByteBuffer.wrap(bufferArray);
long filteredLength = 0;
while (channel.read(buffer) > 0) {
int len = buffer.position();
for (int i = 0; i < len; i++) {
byte b = bufferArray[i];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
filteredLength++;
}
}
buffer.clear();
}
channel.position(startPosition);
Checksum hasher = MurmurHash2.hash32(filteredLength, 1);
while (channel.read(buffer) > 0) {
int len = buffer.position();
int pos = 0;
while (pos < len) {
byte b = bufferArray[pos];
if (b == 0x9 || b == 0xa || b == 0xd || b == 0x20) {
break;
}
pos++;
}
if (pos < len) {
int pos2 = pos + 1;
while (pos2 < len) {
byte b = bufferArray[pos2];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
bufferArray[pos++] = b;
}
pos2++;
}
}
hasher.update(bufferArray, 0, pos);
buffer.clear();
}
return hasher.getValue();
} catch (IllegalArgumentException | IllegalStateException e) {
throw new IOException(e);
}
}
/// Finds the remote CurseForge version matching a local file.
@Override
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
long hash = calculateFingerprint(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream stream = Files.newInputStream(file)) {
byte[] buf = new byte[1024];
int len;
while ((len = stream.read(buf, 0, buf.length)) != -1) {
for (int i = 0; i < len; i++) {
byte b = buf[i];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
baos.write(b);
}
}
}
}
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
if (hash == 811513880) { // Workaround for https://github.com/HMCL-dev/HMCL/issues/4597
return Optional.empty();
}
@@ -420,6 +420,7 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
loaders.stream().flatMap(loader -> {
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
else if ("legacy-fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LEGACY_FABRIC);
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE);
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER);
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.addon.resourcepack;
import com.google.gson.annotations.SerializedName;
import kala.encdet.EncodingDetector;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.addon.LocalAddonManager;
import org.jackhuang.hmcl.addon.meta.PackMcMeta;
@@ -36,6 +37,8 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
@@ -224,33 +227,46 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
this.optionsFile = repository.getRunDirectory(id).resolve("options.txt");
}
private @Nullable Charset optionsFileEncoding;
@NotNull
private Map<String, String> loadOptions() {
getMinecraftVersion();
Map<String, String> options = new LinkedHashMap<>();
if (!Files.isRegularFile(optionsFile)) return options;
try (var stream = Files.lines(optionsFile, StandardCharsets.UTF_8)) {
stream.forEach(s -> {
if (StringUtils.isNotBlank(s)) {
var entry = s.split(":", 2);
if (entry.length == 2) {
options.put(entry[0], entry[1]);
}
}
});
} catch (IOException e) {
byte[] bytes;
try {
bytes = Files.readAllBytes(optionsFile);
} catch (IOException | UncheckedIOException e) {
LOG.warning("Failed to read instance options file", e);
return options;
}
EncodingDetector.Encoding bestEncoding = EncodingDetector.MODERN_WEB.detect(bytes).bestEncoding();
if (bestEncoding == EncodingDetector.Encoding.ASCII)
bestEncoding = EncodingDetector.Encoding.UTF_8;
optionsFileEncoding = bestEncoding != null && bestEncoding.approximateCharset() != null
? bestEncoding.approximateCharset() : StandardCharsets.UTF_8;
//noinspection DataFlowIssue
new String(bytes, optionsFileEncoding).lines().forEach(s -> {
if (StringUtils.isNotBlank(s)) {
var entry = s.split(":", 2);
if (entry.length == 2) {
options.put(entry[0], entry[1]);
}
}
});
return options;
}
private void saveOptions(@NotNull Map<String, String> options) {
StringBuilder sb = new StringBuilder();
for (var entry : options.entrySet()) {
sb.append(entry.getKey()).append(":").append(entry.getValue()).append(System.lineSeparator());
}
try {
StringBuilder sb = new StringBuilder();
for (var entry : options.entrySet()) {
sb.append(entry.getKey()).append(":").append(entry.getValue()).append(System.lineSeparator());
}
FileUtils.saveSafely(optionsFile, sb.toString());
FileUtils.saveSafely(optionsFile, sb.toString(), optionsFileEncoding);
} catch (IOException e) {
LOG.warning("Failed to save instance options file", e);
}
@@ -277,7 +293,8 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
if (requiredVersion == null) {
lock.lock();
try {
if (requiredVersion == null) requiredVersion = getPackVersion(getMinecraftVersion(), repository.getVersionJar(id));
if (requiredVersion == null)
requiredVersion = getPackVersion(getMinecraftVersion(), repository.getVersionJar(id));
} finally {
lock.unlock();
}
@@ -17,11 +17,7 @@
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.zip.Checksum;
/**
* Implementation of the MurmurHash2 32-bit and 64-bit hash functions.
@@ -52,7 +48,6 @@ import java.util.zip.Checksum;
* Original MurmurHash2 c++ code</a>
* @since 1.13
*/
@NotNullByDefault
public final class MurmurHash2 {
// Constants for 32-bit variant
@@ -69,177 +64,6 @@ public final class MurmurHash2 {
private MurmurHash2() {
}
/// Creates a streaming MurmurHash2 32-bit checksum for exactly `length` bytes.
///
/// The length is incorporated into the initial hash state using its low 32 bits. Calls to
/// [Checksum#update(int)] and [Checksum#update(byte[], int, int)] may divide the input at
/// arbitrary byte boundaries. [Checksum#getValue()] returns the hash as an unsigned 32-bit
/// value represented by a `long`, and does not change the checksum state.
///
/// The returned checksum verifies the number of supplied bytes when `getValue()` is called.
/// If too few bytes have been supplied, the caller may continue updating the checksum and call
/// `getValue()` again. Once too many bytes have been supplied, [Checksum#reset()] must be called
/// before a value can be obtained. Resetting retains the configured length and seed. The
/// returned checksum is mutable and is not safe for concurrent use.
///
/// @param length the exact number of input bytes
/// @param seed the initial seed value
/// @return a new checksum initialized with the specified length and seed
/// @throws IllegalArgumentException if `length` is negative
public static Checksum hash32(final long length, final int seed) {
if (length < 0) {
throw new IllegalArgumentException("length must not be negative: " + length);
}
return new Hash32Checksum(length, seed);
}
/// Computes a MurmurHash2 32-bit value from updates whose total length is known in advance.
private static final class Hash32Checksum implements Checksum {
/// The exact number of bytes required before the hash can be obtained.
private final long expectedLength;
/// The hash state restored by [#reset()].
private final int initialHash;
/// The hash state after all complete four-byte blocks received so far.
private int hash;
/// The number of bytes counted before [#inputLengthExceeded] becomes `true`.
private long inputLength;
/// Whether more than [#expectedLength] bytes have been received.
private boolean inputLengthExceeded;
/// Up to three unprocessed bytes packed in little-endian order.
private int tail;
/// The number of bytes currently stored in [#tail].
private int tailLength;
/// Creates a checksum with a precomputed initial hash state.
///
/// @param expectedLength the exact number of input bytes
/// @param seed the initial seed value
private Hash32Checksum(long expectedLength, int seed) {
this.expectedLength = expectedLength;
this.initialHash = seed ^ (int) expectedLength;
this.hash = initialHash;
}
/// Incorporates the low eight bits of `value` into this checksum.
///
/// @param value the value whose low eight bits are incorporated
@Override
public void update(int value) {
addInputLength(1);
appendByte(value);
}
/// Incorporates `length` bytes beginning at `offset` into this checksum.
///
/// @param data the array containing the input bytes
/// @param offset the offset of the first input byte
/// @param length the number of bytes to incorporate
@Override
public void update(byte[] data, int offset, int length) {
Objects.checkFromIndexSize(offset, length, data.length);
addInputLength(length);
int index = offset;
final int end = offset + length;
while (tailLength != 0 && index < end) {
appendByte(data[index++]);
}
while (index <= end - Integer.BYTES) {
mixBlock(ByteArray.getIntLE(data, index));
index += Integer.BYTES;
}
while (index < end) {
appendByte(data[index++]);
}
}
/// Returns the MurmurHash2 value after verifying the exact input length.
///
/// @return the unsigned 32-bit hash value represented by a `long`
/// @throws IllegalStateException if the number of supplied bytes differs from the expected
/// length
@Override
public long getValue() {
if (inputLengthExceeded) {
throw new IllegalStateException(
"Expected " + expectedLength + " bytes, but received more than expected");
}
if (inputLength != expectedLength) {
throw new IllegalStateException(
"Expected " + expectedLength + " bytes, but received " + inputLength);
}
int result = hash;
if (tailLength != 0) {
result ^= tail;
result *= M32;
}
result ^= result >>> 13;
result *= M32;
result ^= result >>> 15;
return Integer.toUnsignedLong(result);
}
/// Restores this checksum to its initial state while retaining its expected length and seed.
@Override
public void reset() {
hash = initialHash;
inputLength = 0;
inputLengthExceeded = false;
tail = 0;
tailLength = 0;
}
/// Records that `length` more input bytes have been supplied.
///
/// @param length the non-negative number of additional bytes
private void addInputLength(int length) {
if (inputLengthExceeded) {
return;
}
if (length > expectedLength - inputLength) {
inputLengthExceeded = true;
} else {
inputLength += length;
}
}
/// Buffers one byte and mixes the resulting block when four bytes are available.
///
/// @param value the value whose low eight bits are appended
private void appendByte(int value) {
tail |= (value & 0xff) << (tailLength * Byte.SIZE);
tailLength++;
if (tailLength == Integer.BYTES) {
mixBlock(tail);
tail = 0;
tailLength = 0;
}
}
/// Mixes one little-endian four-byte block into the current hash state.
///
/// @param block the block to mix
private void mixBlock(int block) {
int mixedBlock = block;
mixedBlock *= M32;
mixedBlock ^= mixedBlock >>> R32;
mixedBlock *= M32;
hash *= M32;
hash ^= mixedBlock;
}
}
/**
* Generates a 32-bit hash from byte array with the given length and seed.
*
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.TypeUtils;
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.NotNullByDefault;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
@@ -57,6 +58,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// All subclasses of this class must call [#register()] once in their constructor.
///
/// @author Glavo
@NotNullByDefault
public abstract class ObservableSetting implements Observable {
private static final ClassValue<List<? extends ObservableField<?>>> FIELDS = new ClassValue<>() {
@@ -94,6 +96,9 @@ public abstract class ObservableSetting implements Observable {
protected transient final Map<String, JsonElement> unknownFields = new HashMap<>();
protected transient final DirtyTracker tracker = new DirtyTracker();
/// Whether a deferred field change still needs to be saved.
private transient boolean savePending;
private boolean registered = false;
protected final void register() {
@@ -111,6 +116,26 @@ public abstract class ObservableSetting implements Observable {
}
}
/// Returns whether a change to the given field should be saved immediately.
///
/// @param observable the changed observable field
/// @return whether the change should trigger listeners immediately
public boolean shouldSaveImmediately(Observable observable) {
return true;
}
/// Returns whether a deferred field change still needs to be saved.
public final boolean isSavePending() {
return savePending;
}
/// Sets whether a deferred field change still needs to be saved.
///
/// @param savePending whether a deferred field change still needs to be saved
public final void setSavePending(boolean savePending) {
this.savePending = savePending;
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
@@ -19,13 +19,16 @@ package org.jackhuang.hmcl.util.io;
import kala.compress.archivers.zip.ZipArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import kala.compress.utils.BufferedDataInputSeekableChannel;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.tree.ZipFileTree;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.*;
import java.nio.file.*;
import java.nio.file.spi.FileSystemProvider;
@@ -128,6 +131,18 @@ public final class CompressingUtils {
throw new IOException("Cannot find suitable encoding for the zip.");
}
private static BufferedDataInputSeekableChannel openBufferedChannel(Path zipFile) throws IOException {
SeekableByteChannel channel = Files.newByteChannel(zipFile);
try {
return new BufferedDataInputSeekableChannel(channel, 8192, ByteOrder.LITTLE_ENDIAN);
} catch (Throwable e) {
IOUtils.closeQuietly(channel, e);
throw e;
}
}
public static ZipFileTree openZipTree(Path zipFile) throws IOException {
return new ZipFileTree(openZipFile(zipFile));
}
@@ -137,14 +152,14 @@ public final class CompressingUtils {
}
public static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException {
return new ZipArchiveReader(zipFile, charset, true, true);
return new ZipArchiveReader(openBufferedChannel(zipFile), charset);
}
public static ZipArchiveReader openZipFileWithPossibleEncoding(Path zipFile, Charset possibleEncoding) throws IOException {
if (possibleEncoding == null)
possibleEncoding = StandardCharsets.UTF_8;
ZipArchiveReader zipReader = new ZipArchiveReader(zipFile, StandardCharsets.UTF_8, true, true);
ZipArchiveReader zipReader = new ZipArchiveReader(openBufferedChannel(zipFile));
Charset suitableEncoding;
try {
@@ -161,7 +176,7 @@ public final class CompressingUtils {
}
zipReader.close();
return new ZipArchiveReader(zipFile, suitableEncoding, true, true);
return new ZipArchiveReader(openBufferedChannel(zipFile), suitableEncoding);
}
public static final class Builder {
@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.util.io;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.glavo.chardet.DetectedCharset;
import org.glavo.chardet.UniversalDetector;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -27,6 +25,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFileAttributeView;
@@ -38,7 +38,6 @@ import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
@@ -236,24 +235,6 @@ public final class FileUtils {
}
}
public static String readTextMaybeNativeEncoding(Path file) throws IOException {
byte[] bytes = Files.readAllBytes(file);
if (OperatingSystem.NATIVE_CHARSET == UTF_8)
return new String(bytes, UTF_8);
UniversalDetector detector = new UniversalDetector();
detector.handleData(bytes);
detector.dataEnd();
DetectedCharset detectedCharset = detector.getDetectedCharset();
if (detectedCharset != null && detectedCharset.isSupported()
&& (detectedCharset == DetectedCharset.UTF_8 || detectedCharset == DetectedCharset.US_ASCII))
return new String(bytes, UTF_8);
else
return new String(bytes, OperatingSystem.NATIVE_CHARSET);
}
public static void deleteDirectory(Path directory) throws IOException {
if (!Files.exists(directory))
return;
@@ -507,13 +488,17 @@ public final class FileUtils {
}
public static void saveSafely(Path file, String content) throws IOException {
saveSafely(file, content, StandardCharsets.UTF_8);
}
public static void saveSafely(Path file, String content, @Nullable Charset charset) throws IOException {
Path parent = file.toAbsolutePath().getParent();
if (parent != null) {
Files.createDirectories(parent);
}
Path tmpFile = tmpSaveFile(file);
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, charset != null ? charset : StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
writer.write(content);
}
@@ -17,19 +17,11 @@
*/
package org.jackhuang.hmcl.util.io;
import org.glavo.chardet.DetectedCharset;
import org.glavo.chardet.UniversalDetector;
import java.io.*;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.GZIPInputStream;
import static java.nio.charset.StandardCharsets.*;
import static org.jackhuang.hmcl.util.platform.OperatingSystem.NATIVE_CHARSET;
/**
* This utility class consists of some util methods operating on InputStream/OutputStream.
@@ -43,25 +35,6 @@ public final class IOUtils {
public static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
public static BufferedReader newBufferedReaderMaybeNativeEncoding(Path file) throws IOException {
if (NATIVE_CHARSET == UTF_8)
return Files.newBufferedReader(file);
FileChannel channel = FileChannel.open(file);
try {
long oldPosition = channel.position();
DetectedCharset detectedCharset = UniversalDetector.detectCharset(channel);
Charset charset = detectedCharset != null && detectedCharset.isSupported()
&& (detectedCharset.getCharset() == UTF_8 || detectedCharset.getCharset() == US_ASCII)
? UTF_8 : NATIVE_CHARSET;
channel.position(oldPosition);
return new BufferedReader(new InputStreamReader(Channels.newInputStream(channel), charset));
} catch (Throwable e) {
closeQuietly(channel, e);
throw e;
}
}
public static byte[] readFully(InputStream stream) throws IOException {
try (stream) {
return stream.readAllBytes();
@@ -394,7 +394,7 @@ public final class NetworkUtils {
StringBuilder sb = new StringBuilder();
if (params != null) {
for (Map.Entry<String, String> e : params.entrySet())
sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
sb.append(encodeURL(e.getKey())).append("=").append(encodeURL(e.getValue())).append("&");
sb.deleteCharAt(sb.length() - 1);
}
return doPost(u, sb.toString());
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util.javafx;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import org.jetbrains.annotations.NotNullByDefault;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -28,6 +29,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
*
* @author yushijinhun
*/
@NotNullByDefault
public final class ObservableHelper implements Observable, InvalidationListener {
private final List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
@@ -63,7 +65,7 @@ public final class ObservableHelper implements Observable, InvalidationListener
@Override
public void invalidated(Observable observable) {
this.invalidate();
listeners.forEach(it -> it.invalidated(observable));
}
public void receiveUpdatesFrom(Observable observable) {
@@ -0,0 +1,48 @@
/*
* 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.windows;
import com.sun.jna.Pointer;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
/// Provides mappings for the GDI functions used by HMCL.
///
/// @author Glavo
@NotNullByDefault
public interface Gdi32 extends StdCallLibrary {
/// The loaded GDI library, or `null` when JNA is unavailable or the current platform is not Windows.
@Nullable Gdi32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
? NativeUtils.load("gdi32", Gdi32.class)
: null;
/// The [GetDeviceCaps](https://learn.microsoft.com/windows/win32/api/wingdi/nf-wingdi-getdevicecaps)
/// index for the current vertical refresh rate, in hertz.
int VREFRESH = 116;
/// Retrieves device-specific information for the supplied device context.
///
/// @param hdc the device context to query
/// @param index the capability index to retrieve
/// @return the value of the requested capability
/// @see <a href="https://learn.microsoft.com/windows/win32/api/wingdi/nf-wingdi-getdevicecaps">GetDeviceCaps function</a>
int GetDeviceCaps(Pointer hdc, int index);
}
@@ -0,0 +1,51 @@
/*
* 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.windows;
import com.sun.jna.Pointer;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
/// Provides mappings for the user interface functions used by HMCL.
///
/// @author Glavo
@NotNullByDefault
public interface User32 extends StdCallLibrary {
/// The loaded User32 library, or `null` when JNA is unavailable or the current platform is not Windows.
@Nullable User32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
? NativeUtils.load("user32", User32.class)
: null;
/// Retrieves a device context for a window or for the entire screen.
///
/// @param hWnd the target window, or `null` to retrieve the screen device context
/// @return the device context, or `null` if the function fails
/// @see <a href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-getdc">GetDC function</a>
@Nullable Pointer GetDC(@Nullable Pointer hWnd);
/// Releases a device context retrieved by {@link #GetDC(Pointer)}.
///
/// @param hWnd the same window passed to {@link #GetDC(Pointer)}, or `null` for a screen device context
/// @param hDC the device context to release
/// @return `1` if the device context was released, or `0` if it was not released
/// @see <a href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-releasedc">ReleaseDC function</a>
int ReleaseDC(@Nullable Pointer hWnd, Pointer hDC);
}
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
@@ -44,7 +44,7 @@ public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Clos
String name = namePath.toString();
if (name.endsWith(".jar") || name.endsWith(".zip")) {
return CompressingUtils.openZipTree(file);
return new ZipFileTree(new ZipArchiveReader(file));
} else if (name.endsWith(".tar") || name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return TarFileTree.open(file);
} else {
@@ -933,21 +933,4 @@
26.2-pre-6
26.2-rc-1
26.2-rc-2
26.2
26.3-snapshot-1
26.3-snapshot-2
26.3-snapshot-3
26.3-snapshot-4
26.3-snapshot-5
26.3-snapshot-6
26.3-snapshot-7
26.3-snapshot-8
26.3-snapshot-9
26.3-snapshot-10
26.3-pre-1
26.3-pre-2
26.3-pre-3
26.3-rc-1
26.3-rc-2
26.3-rc-3
26.3
26.2
@@ -0,0 +1,54 @@
/*
* 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.addon.curse;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.*;
public class CurseForgeRemoteAddonRepositoryTest {
@Test
@Disabled
public void testMurmurHash() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream is = Files.newInputStream(Paths.get("C:\\Users\\huang\\Downloads\\JustEnoughCalculation-1.16.5-3.8.5.jar"))) {
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf, 0, buf.length)) > 0) {
for (int i = 0; i < len; i++) {
byte b = buf[i];
if (b != 9 && b != 10 && b != 13 && b != 32) {
baos.write(b);
}
}
}
}
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
assertEquals(hash, 3333498611L);
}
}
@@ -1,73 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.addon.repository;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
/// Tests CurseForge fingerprint calculation for local files.
@NotNullByDefault
public final class CurseForgeRemoteAddonRepositoryTest {
/// Verifies that streaming calculation remains identical to the previous in-memory algorithm.
@Test
public void calculatesFingerprintWithoutChangingTheResult(@TempDir Path tempDir) throws IOException {
byte[] boundarySample = new byte[8192 + 17];
new Random(0).nextBytes(boundarySample);
for (int i = 0; i < boundarySample.length; i += 97) {
boundarySample[i] = 0x20;
}
byte[][] samples = {
{},
{0x9, 0xa, 0xd, 0x20},
{1},
{1, 2},
{1, 2, 3},
{1, 2, 3, 4},
{1, 0x20, 2, 0xa, 3, 0xd, 4, 0x9, 5},
boundarySample
};
for (int i = 0; i < samples.length; i++) {
byte[] sample = samples[i];
Path file = tempDir.resolve("sample-" + i);
Files.write(file, sample);
ByteArrayOutputStream filtered = new ByteArrayOutputStream();
for (byte b : sample) {
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
filtered.write(b);
}
}
long expected = Integer.toUnsignedLong(MurmurHash2.hash32(filtered.toByteArray(), filtered.size(), 1));
assertEquals(expected, CurseForgeRemoteAddonRepository.calculateFingerprint(file));
}
}
}
@@ -1,272 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
import java.util.zip.Checksum;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Tests the streaming MurmurHash2 checksum implementation.
@NotNullByDefault
public final class MurmurHash2Test {
/// Compares whole-array, bulk, byte-wise, and fragmented updates across lengths and seeds.
@Test
public void testStreamingHash32AcrossLengthsAndSeeds() {
byte[] data = new byte[513];
new Random(0x6d75726d75724cL).nextBytes(data);
for (int length = 0; length <= data.length; length++) {
assertStreamingHash32(data, length, 0);
assertStreamingHash32(data, length, -1);
assertStreamingHash32(data, length, 0x9747b28c);
assertStreamingHash32(data, length, 0x9747b28c ^ length * 31);
}
}
/// Compares every two-part split while reading from a non-zero source-array offset.
@Test
public void testStreamingHash32AtEverySplitPoint() {
byte[] data = new byte[73];
new Random(0x73706c697473L).nextBytes(data);
int sourceOffset = 5;
byte[] paddedData = new byte[sourceOffset + data.length + 7];
System.arraycopy(data, 0, paddedData, sourceOffset, data.length);
for (int length = 0; length <= data.length; length++) {
int seed = Integer.rotateLeft(0x4d757232, length & 31);
long expected = hash32(data, length, seed);
for (int split = 0; split <= length; split++) {
Checksum checksum = MurmurHash2.hash32(length, seed);
checksum.update(paddedData, sourceOffset, split);
checksum.update(paddedData, sourceOffset + split, length - split);
assertEquals(expected, checksum.getValue(),
"length=" + length + ", split=" + split);
}
}
}
/// Compares interleaved single-byte and bulk updates after every possible short prefix.
@Test
public void testStreamingHash32WithMixedUpdates() {
byte[] data = new byte[129];
new Random(0x6d69786564L).nextBytes(data);
for (int length = 0; length <= data.length; length++) {
int seed = 0x13579bdf ^ length;
long expected = hash32(data, length, seed);
for (int prefixLength = 0; prefixLength <= Math.min(length, 7); prefixLength++) {
Checksum checksum = MurmurHash2.hash32(length, seed);
int offset = 0;
while (offset < prefixLength) {
checksum.update(data[offset++]);
}
// Exercise an empty bulk update both with and without a buffered tail.
checksum.update(data, offset, 0);
while (offset < length) {
int chunkLength = Math.min(offset % 5 + 1, length - offset);
checksum.update(data, offset, chunkLength);
offset += chunkLength;
if (offset < length) {
checksum.update(data[offset++]);
}
}
assertEquals(expected, checksum.getValue(),
"length=" + length + ", prefixLength=" + prefixLength);
}
}
}
/// Compares array-backed, read-only, direct, and empty-buffer updates.
@Test
public void testStreamingHash32WithByteBuffers() {
byte[] data = new byte[8207];
new Random(0x627566666572L).nextBytes(data);
int seed = 0x2468ace0;
long expected = hash32(data, data.length, seed);
int sourceOffset = 3;
byte[] paddedData = new byte[sourceOffset + data.length + 4];
System.arraycopy(data, 0, paddedData, sourceOffset, data.length);
ByteBuffer arrayBackedBuffer = ByteBuffer.wrap(paddedData);
arrayBackedBuffer.position(sourceOffset);
arrayBackedBuffer.limit(sourceOffset + data.length);
assertTrue(arrayBackedBuffer.hasArray());
Checksum arrayBackedChecksum = MurmurHash2.hash32(data.length, seed);
arrayBackedChecksum.update(arrayBackedBuffer);
assertEquals(arrayBackedBuffer.limit(), arrayBackedBuffer.position());
assertEquals(expected, arrayBackedChecksum.getValue());
ByteBuffer readOnlyBuffer = ByteBuffer.wrap(data).asReadOnlyBuffer();
assertFalse(readOnlyBuffer.hasArray());
Checksum readOnlyChecksum = MurmurHash2.hash32(data.length, seed);
readOnlyChecksum.update(readOnlyBuffer);
assertEquals(readOnlyBuffer.limit(), readOnlyBuffer.position());
assertEquals(expected, readOnlyChecksum.getValue());
ByteBuffer directBuffer = ByteBuffer.allocateDirect(sourceOffset + data.length);
directBuffer.position(sourceOffset);
directBuffer.put(data);
directBuffer.position(sourceOffset);
assertFalse(directBuffer.hasArray());
Checksum directChecksum = MurmurHash2.hash32(data.length, seed);
directChecksum.update(directBuffer);
assertEquals(directBuffer.limit(), directBuffer.position());
assertEquals(expected, directChecksum.getValue());
ByteBuffer emptyBuffer = ByteBuffer.allocate(4);
emptyBuffer.position(2);
emptyBuffer.limit(2);
Checksum emptyFirstChecksum = MurmurHash2.hash32(data.length, seed);
emptyFirstChecksum.update(emptyBuffer);
emptyFirstChecksum.update(data);
assertEquals(2, emptyBuffer.position());
assertEquals(expected, emptyFirstChecksum.getValue());
}
/// Verifies that single-byte updates use only the low eight bits of each value.
@Test
public void testStreamingHash32SingleByteValues() {
byte[] data = {0x12, (byte) 0xff, (byte) 0x80, 0x00};
int seed = 0x12345678;
Checksum checksum = MurmurHash2.hash32(data.length, seed);
checksum.update(0x112);
checksum.update(-1);
checksum.update(0xabcdef80);
checksum.update(0x100);
assertEquals(hash32(data, data.length, seed), checksum.getValue());
}
/// Verifies that rejected array ranges leave the checksum state unchanged.
@Test
public void testStreamingHash32ArrayRangeValidation() {
byte[] data = {0x12, 0x34, 0x56};
int seed = 0x12345678;
Checksum checksum = MurmurHash2.hash32(data.length, seed);
assertThrows(NullPointerException.class, () -> checksum.update(null, 0, 0));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, -1, 1));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, 0, -1));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, data.length, 1));
checksum.update(data);
assertEquals(hash32(data, data.length, seed), checksum.getValue());
}
/// Verifies that `getValue()` rejects incomplete and excessive input without finalizing state.
@Test
public void testExactLengthValidation() {
byte[] data = {0x12, 0x34, 0x56, 0x78, (byte) 0x9a};
int seed = 0x12345678;
long expected = hash32(data, data.length, seed);
Checksum checksum = MurmurHash2.hash32(data.length, seed);
checksum.update(data, 0, data.length - 1);
assertThrows(IllegalStateException.class, checksum::getValue);
checksum.update(data[data.length - 1]);
assertEquals(expected, checksum.getValue());
checksum.update(0xff);
checksum.update(0xfe);
assertThrows(IllegalStateException.class, checksum::getValue);
checksum.reset();
checksum.update(data, 0, data.length);
assertEquals(expected, checksum.getValue());
}
/// Verifies the valid range of the declared input length.
@Test
public void testLengthRange() {
assertThrows(IllegalArgumentException.class, () -> MurmurHash2.hash32(-1, 0));
Checksum checksum = MurmurHash2.hash32(1L << 32, 0);
assertThrows(IllegalStateException.class, checksum::getValue);
}
/// Compares the principal update styles with the byte-array implementation.
///
/// @param data the input array
/// @param length the number of bytes to hash
/// @param seed the initial seed value
private static void assertStreamingHash32(byte[] data, int length, int seed) {
long expected = hash32(data, length, seed);
byte[] exactData = Arrays.copyOf(data, length);
Checksum wholeArray = MurmurHash2.hash32(length, seed);
wholeArray.update(exactData);
assertEquals(expected, wholeArray.getValue());
Checksum bulk = MurmurHash2.hash32(length, seed);
bulk.update(data, 0, length);
assertEquals(expected, bulk.getValue());
assertEquals(expected, bulk.getValue());
Checksum byteWise = MurmurHash2.hash32(length, seed);
for (int i = 0; i < length; i++) {
byteWise.update(data[i]);
}
assertEquals(expected, byteWise.getValue());
Checksum fragmented = MurmurHash2.hash32(length, seed);
int offset = 0;
while (offset < length) {
int chunkLength = Math.min(offset % 7 + 1, length - offset);
fragmented.update(data, offset, chunkLength);
offset += chunkLength;
}
assertEquals(expected, fragmented.getValue());
}
/// Returns the unsigned value produced by the byte-array implementation.
///
/// @param data the input array
/// @param length the number of bytes to hash
/// @param seed the initial seed value
/// @return the unsigned 32-bit hash value represented by a `long`
private static long hash32(byte[] data, int length, int seed) {
return Integer.toUnsignedLong(MurmurHash2.hash32(data, length, seed));
}
/// Prevents instantiation.
private MurmurHash2Test() {
}
}
-1
View File
@@ -12,7 +12,6 @@ dependencies {
implementation(libs.jna)
implementation(libs.kala.compress.tar)
implementation(libs.kala.compress.ar)
implementation(libs.weburl)
}
java {
@@ -57,7 +57,7 @@ public final class JavaFXPlatform {
ALL.put("linux-riscv64", new JavaFXPlatform("linux", GLAVO_GROUP_ID, "19.0.2.1-riscv64"));
// FreeBSD
ALL.put("freebsd-x86_64", new JavaFXPlatform("freebsd", GLAVO_GROUP_ID, "14.0.2.1-freebsd"));
ALL.put("freebsd-x86_64", new JavaFXPlatform("freebsd", GLAVO_GROUP_ID, "21.0.11-freebsd"));
}
private final String classifier;
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.gradle.mod;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import org.glavo.url.WebURL;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
@@ -34,6 +33,7 @@ import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@@ -58,7 +58,7 @@ public abstract class ParseModDataTask extends DefaultTask {
private static final String MOD_SEPARATOR = ",";
private static final Pattern[] CURSEFORGE_PATTERNS = {
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts|bukkit-plugins)/+(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts)/+(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/projects/(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/mc-mods/minecraft/(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/legacy/mc-mods/minecraft/(\\d+)-(?<modid>[\\w-]+)"),
@@ -70,6 +70,27 @@ public abstract class ParseModDataTask extends DefaultTask {
.replace("&gt;", ">");
}
private static String parseCurseforge(String url) {
URI res = URI.create(url.replace(" ", "%20"));
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
return "";
}
if ("edge.forgecdn.net".equals(res.getHost())) {
return "";
}
for (Pattern pattern : CURSEFORGE_PATTERNS) {
Matcher matcher = pattern.matcher(res.getPath());
if (matcher.matches()) {
return matcher.group("modid");
}
}
return "";
}
private static final Pattern MCMOD_PATTERN =
Pattern.compile("^https://www\\.mcmod\\.cn/(class|modpack)/(?<modid>\\d+)\\.html$");
@@ -148,8 +169,7 @@ public abstract class ParseModDataTask extends DefaultTask {
}
if (chineseName.contains(S) || subName.contains(S)) {
LOGGER.warn("Error chinese name: {}", chineseName);
continue;
throw new GradleException("Error: " + chineseName);
}
String curseforgeId = "";
@@ -160,40 +180,13 @@ public abstract class ParseModDataTask extends DefaultTask {
List<ModData.Link> mcmodLinks = links.get("mcmod");
if (curseforgeLinks != null && !curseforgeLinks.isEmpty()) {
boolean reportError = true;
for (ModData.Link link : curseforgeLinks) {
WebURL res = WebURL.parse(link.url);
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
curseforgeId = "";
break;
}
if (Set.of(
"files.xmdhs.com",
"edge.forgecdn.net",
"minecraft.curseforge.com",
"mediafilez.forgecdn.net"
).contains(res.getHost())) {
reportError = false;
curseforgeId = "";
break;
}
for (Pattern pattern : CURSEFORGE_PATTERNS) {
Matcher matcher = pattern.matcher(res.getPath());
if (matcher.matches()) {
curseforgeId = matcher.group("modid");
break;
}
}
curseforgeId = parseCurseforge(link.url);
if (!curseforgeId.isEmpty()) {
break;
}
}
if (curseforgeId.isEmpty() && reportError) {
if (curseforgeId.isEmpty()) {
LOGGER.warn("Error curseforge: {}", chineseName);
}
}
+1 -1
View File
@@ -15,4 +15,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
versionRoot=3.16
versionRoot=3.17
+4 -4
View File
@@ -1,7 +1,8 @@
[versions]
hmclauncher = "3.7.0.1"
jetbrains-annotations = "26.1.0"
kala-compress = "1.27.1-5"
kala-compress = "1.27.1-3"
kala-encoding-detctor = "0.1.0"
simple-png-javafx = "0.3.0"
gson = "2.13.2"
tomlj = "1.1.1"
@@ -12,8 +13,7 @@ constant-pool-scanner = "1.2"
hello-nbt = "0.3.0"
nanohttpd = "2.3.1"
jsoup = "1.21.2"
chardet = "2.5.0"
fxsvgimage = "1.9.1"
fxsvgimage = "1.3"
jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
@@ -39,6 +39,7 @@ jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "j
kala-compress-zip = { module = "org.glavo.kala:kala-compress-archivers-zip", version.ref = "kala-compress" }
kala-compress-tar = { module = "org.glavo.kala:kala-compress-archivers-tar", version.ref = "kala-compress" }
kala-compress-ar = { module = "org.glavo.kala:kala-compress-archivers-ar", version.ref = "kala-compress" }
kala-encoding-detctor = { module = "org.glavo:kala-encoding-detector", version.ref = "kala-encoding-detctor" }
simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "simple-png-javafx" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }
@@ -49,7 +50,6 @@ constant-pool-scanner = { module = "org.jenkins-ci:constant-pool-scanner", versi
hello-nbt = { module = "org.glavo:HelloNBT", version.ref = "hello-nbt" }
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }