Compare commits

..
74 Commits
Author SHA1 Message Date
Glavo fbcc17472f [release/3.16] Add version entries for 26.3 snapshots and release candidates (#6879)
https://github.com/HMCL-dev/HMCL/pull/6849
https://github.com/HMCL-dev/HMCL/pull/6877
2026-09-16 21:48:50 +08:00
Glavo 7b85f0dfce [release/3.16] 优化加载模组列表的性能 (#6878)
https://github.com/HMCL-dev/HMCL/pull/6834
https://github.com/HMCL-dev/HMCL/pull/6837
2026-09-16 21:45:50 +08:00
GlavoandApostrophe b8a830dd40 [release/3.16] 解决导出 Modrinth 整合包因大文件导致的 Out Of Memory (#6876)
https://github.com/HMCL-dev/HMCL/pull/6631

Co-authored-by: Apostrophe <Hylfrd@outlook.com>
2026-09-16 21:35:00 +08:00
GlavoandToobLac ce368caa67 [release/3.16] [Bugfix] 修复无法读取 zip 后缀名的模组的问题 (#6875)
https://github.com/HMCL-dev/HMCL/pull/6712

Co-authored-by: ToobLac <tooblac39@gmail.com>
2026-09-16 21:29:04 +08:00
Glavo 850ad82aa1 [release/3.16] fix: LegacyFabric 实例图标异常 (#6874)
https://github.com/HMCL-dev/HMCL/pull/6784
2026-09-16 21:22:58 +08:00
Glavo 552276e673 [release/3.16] 优化 SVG 图像支持 (#6873)
https://github.com/HMCL-dev/HMCL/pull/6613
https://github.com/HMCL-dev/HMCL/pull/6789
https://github.com/HMCL-dev/HMCL/pull/6790
2026-09-16 21:19:57 +08:00
Glavo e79855685e [release/3.16] 更新模组数据 (2026-07-21) (#6872)
https://github.com/HMCL-dev/HMCL/pull/6543
2026-09-16 21:14:22 +08:00
Glavo ad5afe7e06 [release/3.16] Add support for Minecraft 26.3-snapshot-10+ to unofficial supported platforms (#6871)
https://github.com/HMCL-dev/HMCL/pull/6870
2026-09-16 21:05:11 +08:00
GlavoandApostrophe 8210c77de9 [release/3.16] 修复游戏 Java 路径显示滞后问题 (#6508)
https://github.com/HMCL-dev/HMCL/pull/6419

Co-authored-by: Apostrophe <Hylfrd@outlook.com>
2026-07-25 21:39:06 +08:00
Glavo b11b298312 [release/3.16] 更新内置主题相关译文 (#6507)
https://github.com/HMCL-dev/HMCL/pull/6429
2026-07-25 21:34:11 +08:00
Glavoand辞庐 5c605c58d2 [release/3.16] 修复 GameAdvancedListItem#loadVersion UI 操作可能不会 JavaFX 线程运行的问题 (#6506)
https://github.com/HMCL-dev/HMCL/pull/6445

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-07-25 21:32:06 +08:00
Glavo 01d4100a2f [release/3.16] 复制实例时同步保存游戏设置 (#6503)
https://github.com/HMCL-dev/HMCL/pull/6492
2026-07-25 21:26:38 +08:00
Glavoand辞庐 d07eb2f748 [release/3.16] fix: 切换版本隔离设置后自动刷新其他子页面 功能失效 (#6505)
https://github.com/HMCL-dev/HMCL/pull/6468

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-07-25 21:26:09 +08:00
Glavo 345bb176fd [release/3.16] Add support for Minecraft 26.3-snapshot-4+ to unofficial supported platforms (#6504)
https://github.com/HMCL-dev/HMCL/pull/6425
https://github.com/HMCL-dev/HMCL/pull/6481
2026-07-25 21:23:57 +08:00
Glavo 83a38fadf0 [release/3.16] 完善游戏设置页面帮助文本 (#6416)
https://github.com/HMCL-dev/HMCL/pull/6415
2026-07-15 21:22:17 +08:00
Glavoand辞庐 9980dc8aea [release/3.16] Revert "Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin" (#6413)
https://github.com/HMCL-dev/HMCL/pull/6410

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-07-15 20:54:59 +08:00
GlavoandToobLac 7dfbf76b69 [release/3.16] 修复 1.21.1 安装信雅互联仍显示不支持 Fabric 模组的问题 (#6414)
https://github.com/HMCL-dev/HMCL/pull/6404

Co-authored-by: ToobLac <tooblac39@gmail.com>
2026-07-15 20:54:45 +08:00
Glavo 7f2f77f54f [release/3.16] Bump Gradle Wrapper Neo to 0.2.0 (#6412)
https://github.com/HMCL-dev/HMCL/pull/6397
2026-07-15 20:07:13 +08:00
辞庐 2dccd8099d Bump authlib-injector to 1.2.8 (#6390) 2026-07-13 20:33:23 +08:00
Glavo d8a03cfed8 支持主题色跟随系统 (#6385) 2026-07-13 03:42:50 +08:00
Glavo 26a874a030 Bump Gradle to 9.6.1 (#6388) 2026-07-13 03:00:15 +08:00
Glavo 7c5c3e2fb4 使用 Gradle Wrapper Neo 代替 Gradle Wrapper (#6387) 2026-07-13 02:52:47 +08:00
Glavo 62d7b36248 添加“窗口透明”选项 (#6383) 2026-07-13 00:41:12 +08:00
Glavo dec6837528 修复 Minecraft 26.1+ 无法安装 Quilt 的问题 (#6386) 2026-07-13 00:38:52 +08:00
快乐凤爪王 3484792e81 Feat: Added status label opacity when installer item is incompatible (#6335) 2026-07-12 22:37:57 +08:00
快乐凤爪王 2b310e035d Fix: Lock AccountListItemSkin.btnRefresh width to prevent layout shifting (#6369) 2026-07-12 22:01:34 +08:00
Glavo 6e960612dd 固定默认游戏文件夹和官方启动器游戏文件夹的 ID (#6382) 2026-07-12 22:00:30 +08:00
辞庐 73a776b021 fix: 整合包安装界面高度限制失效 (#6371) 2026-07-12 21:46:46 +08:00
辞庐and3gf8jv4dv 31a2f49ed2 更新 fatal.unsupported_platform 设置指引 (#6251)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-07-12 20:49:51 +08:00
快乐凤爪王 ab268cfe48 Feat: DisplayName for JavaPackageType (#6377) 2026-07-12 20:35:32 +08:00
Glavo 721aeccfc0 修复 JFXPasswordField 导致 CPU 占用率异常增高的问题 (#6373) 2026-07-12 01:35:01 +08:00
Glavo e4348f727a 优化下载线程数设置 UI (#6374) 2026-07-12 01:34:17 +08:00
快乐凤爪王 dfc96f10da Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin (#6370) 2026-07-11 21:19:37 +08:00
快乐凤爪王 a5c7fe08d8 Feat: No results found tip for ModDownloadListPage (#6368) 2026-07-11 02:37:09 +08:00
Glavo a6e0acca45 在滚动条处于按下状态时保持不透明状态 (#6366) 2026-07-10 23:57:20 +08:00
Glavo 3d312f9247 修复在 JFXComboBox 弹出窗口的滚动条上释放鼠标时也会导致弹出窗口隐藏的问题 (#6365) 2026-07-10 21:44:51 +08:00
Glavo 18dc5b69c0 Fix #6320: 修复安装新实例时,Fabric API 安装路径未遵循版本隔离策略的问题 (#6355) 2026-07-10 20:36:29 +08:00
快乐凤爪王 98af2c85b6 Feat: Animate menu button arrow counter-clockwise on popup display (#6344) 2026-07-10 20:35:39 +08:00
Glavo c4c0b1a1ce 修复自动分配内存逻辑异常的问题 (#6356) 2026-07-10 20:21:44 +08:00
辞庐 5e621ce340 HttpRequest 添加 PUT 与 DELETE 请求支持 (#6312) 2026-07-09 21:02:14 +08:00
快乐凤爪王 1d1c8dfdc9 Fix: Adjusted the size of the ModInfoListCell ResourcePackListCell icon (#6347) 2026-07-09 20:55:33 +08:00
快乐凤爪王 9b68b8825d Chore: Removed unnecessary punctuation marks of allow_auto_agent.subtitle (#6346) 2026-07-09 20:55:11 +08:00
快乐凤爪王 5c17783223 Fix: Restore subtitle when RemoteVersionListCell is reused (#6329) 2026-07-08 22:03:59 +08:00
Glavo a921be45f1 Fix #6290: GameAdvancedListItem 应等待 HMCLGameRepository 加载完成后再加载图标 (#6325) 2026-07-08 21:59:06 +08:00
辞庐 2dd1c0f85e 优化日志窗口配色 (#6313) 2026-07-08 21:54:24 +08:00
快乐凤爪王 7ba229b3e5 Fix: Adjust GameListCell spacing (#6337) 2026-07-08 21:47:49 +08:00
辞庐 59a299630b fix: #6328 MCIM 描述没有对齐 (#6333) 2026-07-08 21:46:26 +08:00
快乐凤爪王 b41b89e233 Feat: Added numbered square tag to Java runtime list cells (#6334) 2026-07-08 21:40:50 +08:00
ToobLac af137e5ff1 [Bugfix] 修复 #6338: macOS 导出整合包默认名称包含2个后缀名 (#6340)
Fixes #6338
2026-07-08 21:39:37 +08:00
Glavo 74ddbf5623 Streamline memory allocation options and improve preset management (#6330)
Fix #6307
2026-07-08 21:15:36 +08:00
快乐凤爪王 accef4a93f Fix: WorldListPage MDListCell tags (#6339) 2026-07-08 20:37:16 +08:00
快乐凤爪王 9c3a955fad Refactor: Launch pane button (#6341) 2026-07-08 20:27:35 +08:00
Glavo 1c0e506d34 Enhance renderer selection based on game version (#6316) 2026-07-05 20:42:24 +08:00
庐庐绫 6374ec0de7 fix: 修复联机核心第三方下载渠道按钮高度不一致 (#6247)
close #6245
2026-07-05 20:06:23 +08:00
快乐凤爪王 a6b3872af4 Fix: Fixed truncated authlib injector warning text (#6301) 2026-07-05 20:06:02 +08:00
辞庐 ad98a5a6d4 HMCL ProcessListener 游戏退出日志行开头添加时间 (#6314) 2026-07-05 20:04:51 +08:00
快乐凤爪王 05393af537 Fix: Adjust account item spacing (#6302) 2026-07-05 19:57:14 +08:00
Glavo 3438da2ab9 Add game version handling for graphics backend visibility (#6300) 2026-07-05 19:56:02 +08:00
辞庐 3163b465ec fix: 拷贝 uuid 按钮冗余 SpinnerPane (#6315) 2026-07-05 19:54:19 +08:00
辞庐 573dac471c 禁用密码框右键菜单的 剪切 & 复制 按钮 (#6310) 2026-07-05 19:53:43 +08:00
ToobLacandGlavo 12f54f353a [Bugfix] 修复 CF 文件下载未带有 api key 的问题 (#6297)
Fixes #6293

Co-authored-by: Glavo <zjx001202@gmail.com>
2026-07-03 21:26:58 +08:00
Glavo 246699d571 refactor: update preset management terminology for clarity (#6298) 2026-07-03 21:26:14 +08:00
Haoran Xu 3480e6974b Docs: synchronize localized README content (#6235) 2026-07-03 21:12:23 +08:00
Zkitefly 6d14351833 feat(mod): 支持读取Forge新模组元数据中的logo文件 (#6253) 2026-07-03 20:22:24 +08:00
Glavo fd0d83257d 调整旧版实例设置迁移与默认父预设解析逻辑 (#6287) 2026-07-03 20:19:20 +08:00
ToobLac 0eb1d3cf4b [Enhancement] 无论何时都显示关闭愚人节模式的按钮 (#6105)
Closes #5982
2026-07-02 21:54:39 +08:00
辞庐 f9b8dad3d2 修复安装手动打包整合包时任务名未本地化的问题 (#6248) 2026-07-02 21:50:49 +08:00
Glavo 57fbd0910e 不再将从旧版本游戏文件夹设置中迁移出的游戏预设名称设置为文件夹名称 (#6292) 2026-07-02 21:47:51 +08:00
快乐凤爪王 7767a57f50 Fix: Restrict JVM memory text fields to numeric input only (#6260) 2026-07-02 21:46:11 +08:00
快乐凤爪王 f024dba940 Fix: Dynamically lock fixedCellSize of VersionsPageSkin (#6288) 2026-07-02 21:15:03 +08:00
Wulian233 52736d3c86 导出主题时支持设置主题版本 (#6284) 2026-07-02 21:06:36 +08:00
ToobLac f75b472068 [Chores] 其他类向记录的转换 (#6277) 2026-07-02 21:03:36 +08:00
ToobLac d8e0c9e9c7 [Chore] 优化default_addon_source的中文翻译 (#6278) 2026-07-01 20:38:04 +08:00
快乐凤爪王 9601c1c1ea Fix: Aligned the height of the jfx-color-picker with the jfx-text-field (#6285) 2026-07-01 20:37:30 +08:00
160 changed files with 13809 additions and 2724 deletions

No files matched your search

@@ -38,7 +38,6 @@ import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
@@ -68,14 +67,18 @@ public class JFXPasswordField extends PasswordField {
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
}
useJFXContextMenu(this);
}
/// Prevents the legacy JFoenix skin from continuously requesting another layout pass.
// https://github.com/HMCL-dev/HMCL/issues/5822
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
@Override
protected void layoutChildren() {
super.layoutChildren();
this.setNeedsLayout(false);
}
/**
* Initialize the style class to 'jfx-password-field'.
* <p>
@@ -294,4 +297,3 @@ public class JFXPasswordField extends PasswordField {
}
}
@@ -15,9 +15,13 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
@@ -25,6 +29,8 @@ import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import java.util.List;
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
protected final ObjectProperty<Paint> promptTextFill;
private boolean invalid = true;
@@ -150,6 +156,30 @@ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
}
});
for (Node child : getChildren()) {
if (child instanceof ListView<?> listView && "list-view".equals(listView.getId())) {
// The default hideOnClick logic of ComboBoxListViewSkin doesn't work well with FloatScrollBarSkin.
// We implement hideOnClick ourselves to prevent the popup from
// hiding when releasing the mouse on the scrollbar.
this.setHideOnClick(false);
listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
EventTarget target = t.getTarget();
if (target instanceof Node targetNode) {
List<String> s = targetNode.getStyleClass();
if (s.contains("thumb")
|| s.contains("track")
|| s.contains("decrement-arrow")
|| s.contains("increment-arrow")) {
return;
}
}
comboBox.hide();
});
}
}
}
protected void layoutChildren(double x, double y, double w, double h) {
@@ -242,6 +242,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
clean(getRunDirectory(id));
}
/// Removes a version from disk and drops any cached instance settings for that version.
@Override
public boolean removeVersionFromDisk(String id) {
boolean removed = super.removeVersionFromDisk(id);
if (removed) {
instanceGameSettings.remove(id);
loadedInstanceGameSettings.remove(id);
readOnlyInstanceGameSettings.remove(id);
beingModpackVersions.remove(id);
}
return removed;
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
@@ -284,7 +297,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
newGameSettings.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
newGameSettings.runningDirectoryProperty().setValue("");
initInstanceGameSettings(dstId, newGameSettings);
saveGameSettings(dstId);
saveGameSettingsSync(dstId);
Path dstGameDir = getRunDirectory(dstId);
@@ -337,10 +350,15 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return;
}
@Nullable GameSettingsPresetID legacyParent = gameDirectory.getLegacyGameSettings();
if (SettingsManager.getGameSettings(legacyParent) == null) {
legacyParent = null;
}
LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult =
LegacyGameSettingsMigrator.migrateInstanceGameSettings(
this, id,
getParentGameSettings(null).idProperty().getValue());
legacyParent);
if (migrationResult != null) {
initInstanceGameSettings(id, migrationResult.setting());
try {
@@ -351,14 +369,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
return;
}
GameSettings.Preset gameDirectoryPreset = SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
if (gameDirectoryPreset != null && gameDirectoryPreset.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS) {
GameSettings.Instance setting = new GameSettings.Instance();
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
initInstanceGameSettings(id, setting);
saveGameSettings(id);
}
}
/// Loads a new-format instance game settings file.
@@ -385,8 +395,9 @@ public final class HMCLGameRepository extends DefaultGameRepository {
+ file + ", Actual: " + schemaResult.actual());
case UNEXPECTED_ID -> LOG.warning("Unexpected instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA -> LOG.warning("Unsupported instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA ->
LOG.warning("Unsupported instance game settings schema. Expected: "
+ GameSettings.Instance.CURRENT_SCHEMA + ", Actual: " + schemaResult.actual());
case READ_WRITE, READ_WRITE_PRESERVE_SCHEMA -> {
}
}
@@ -516,40 +527,44 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
}
/// Returns the explicit parent preset of the instance, falling back to the default preset.
public GameSettings.Preset getParentGameSettings(@Nullable GameSettings.Instance instance) {
@Nullable GameSettingsPresetID parent = instance != null && instance.parentProperty().getValue() != null
? instance.parentProperty().getValue()
: gameDirectory.getLegacyGameSettings();
@Nullable GameSettingsPresetID parent = instance != null ? instance.parentProperty().getValue() : null;
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
public boolean shouldIsolateNewInstance(boolean modded) {
GameSettings.Preset preset = getParentGameSettings(null);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
return switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> modded;
};
}
/// Applies default isolation to a new instance before the version metadata is saved.
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
return;
}
GameSettings.Instance setting = getInstanceGameSettings(id);
if (setting == null) {
setting = initInstanceGameSettings(id, new GameSettings.Instance());
}
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
saveGameSettings(id);
}
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
}
public void applyDefaultIsolationSetting(String id) {
if (!hasVersion(id)) {
return;
}
GameSettings.Preset preset = getParentGameSettings(null);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
boolean isolated = switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this));
};
if (isolated) {
GameSettings.Instance setting = getInstanceGameSettingsOrCreate(id);
if (setting != null) {
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
}
}
}
public Optional<Path> getVersionIconFile(String id) {
Path root = getVersionRoot(id);
@@ -687,7 +702,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
/// Result of loading an instance-specific game settings file.
///
/// @param setting the loaded instance settings, or `null` when unavailable
/// @param setting the loaded instance settings, or `null` when unavailable
/// @param allowSave whether the file may be overwritten
private record InstanceGameSettingsLoadResult(
@Nullable GameSettings.Instance setting,
@@ -697,7 +712,17 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
GameSettings.Effective vs = getEffectiveGameSettings(version);
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
boolean autoMemory = vs.get(GameSettings::autoMemoryProperty);
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version));
@Nullable Integer maxMemory;
if (autoMemory) {
maxMemory = noJVMOptions
? null
: Math.toIntExact(getAutoAllocatedMemory(SystemInfo.getPhysicalMemoryStatus().available()) / 1024L / 1024L);
} else {
maxMemory = vs.getMaxMemory();
}
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
@@ -705,17 +730,13 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setVersionType(Metadata.TITLE)
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.get(GameSettings::gameArgumentsProperty)))
.setOverrideJavaArguments(StringUtils.tokenize(vs.get(GameSettings::jvmOptionsProperty)))
.setMaxMemory(noJVMOptions && autoMemory ? null : (int) (getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
autoMemory
) / 1024 / 1024))
.setMinMemory(vs.get(GameSettings::minMemoryProperty))
.setMetaspace(Lang.toIntOrNull(vs.get(GameSettings::permSizeProperty)))
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
.setMaxMemory(maxMemory)
.setMinMemory(vs.getInheritable(GameSettings::minMemoryProperty))
.setMetaspace(Lang.toIntOrNull(vs.getInheritable(GameSettings::permSizeProperty)))
.setEnvironmentVariables(
Lang.mapOf(StringUtils.tokenize(vs.get(GameSettings::environmentVariablesProperty))
Lang.mapOf(StringUtils.tokenize(vs.getInheritable(GameSettings::environmentVariablesProperty))
.stream()
.map(it -> {
int idx = it.indexOf('=');
@@ -733,16 +754,16 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setPostExitCommand(vs.getInheritable(GameSettings::postExitCommandProperty))
.setNoGeneratedJVMArgs(noJVMOptions)
.setNoGeneratedOptimizingJVMArgs(vs.getInheritable(GameSettings::noOptimizingJVMOptionsProperty))
.setUseCustomNatives(vs.get(GameSettings::useCustomNativesProperty))
.setNativesDir(vs.get(GameSettings::nativesDirectoryProperty))
.setUseCustomNatives(vs.getInheritable(GameSettings::useCustomNativesProperty))
.setNativesDir(vs.getInheritable(GameSettings::nativesDirectoryProperty))
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
.setRenderer(vs.getRenderer())
.setRenderer(vs.getRenderer(gameVersionNumber))
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
.setUseNativeGLFW(vs.get(GameSettings::useNativeGLFWProperty))
.setUseNativeOpenAL(vs.get(GameSettings::useNativeOpenALProperty))
.setUseNativeGLFW(vs.getInheritable(GameSettings::useNativeGLFWProperty))
.setUseNativeOpenAL(vs.getInheritable(GameSettings::useNativeOpenALProperty))
.setDaemon(!makeLaunchScript && vs.getInheritable(GameSettings::launcherVisibilityProperty).isDaemon())
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
@@ -840,22 +861,21 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
public static long getAutoAllocatedMemory(long available) {
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (usable <= 0) {
return available;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested;
if (usable <= threshold)
suggested = (long) (usable * 0.8);
else
suggested = Math.min(
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return suggested;
}
public static ProxyOption getProxyOption() {
@@ -34,7 +34,10 @@ import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
import org.jackhuang.hmcl.modpack.ModpackProvider;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.JavaVersionType;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
@@ -57,6 +60,7 @@ import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
@@ -71,6 +75,7 @@ import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.TIME_FORMATTER;
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
public final class LauncherHelper {
@@ -172,7 +177,7 @@ public final class LauncherHelper {
}),
Task.composeAsync(() -> {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|| !(setting.getRenderer(GameVersionNumber.asGameVersion(gameVersion)) instanceof Renderer.Driver renderer)
|| renderer.mesaDriverName() == null)
return null;
@@ -539,7 +544,7 @@ public final class LauncherHelper {
}
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (!setting.get(GameSettings::useCustomNativesProperty)) {
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
@@ -576,7 +581,7 @@ public final class LauncherHelper {
}
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
if (java.getBits() == Bits.BIT_32 && !setting.getInheritable(GameSettings::autoMemoryProperty) && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
@@ -623,7 +628,7 @@ public final class LauncherHelper {
suggestions.add(i18n("launch.advice.modlauncher8"));
break;
case VANILLA_X86:
if (!setting.get(GameSettings::useCustomNativesProperty)
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)
&& Platform.isSupportedTranslationX86_64()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
@@ -635,7 +640,7 @@ public final class LauncherHelper {
// Cannot allocate too much memory exceeding free space.
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
if (totalMemorySizeMB > 0 && !setting.getInheritable(GameSettings::autoMemoryProperty) && totalMemorySizeMB < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
}
@@ -964,7 +969,7 @@ public final class LauncherHelper {
@Override
public void onExit(int exitCode, ExitType exitType) {
if (showLogs) {
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
logBuffer.add(new Log(String.format("[%s] [HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", TIME_FORMATTER.format(Instant.now()), exitCode, exitCode, exitType), Log4jLevel.INFO));
submitLogThread.interrupt();
try {
submitLogThread.join();
@@ -26,6 +26,8 @@ import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Path zipFile;
@@ -36,6 +38,8 @@ public class ManuallyCreatedModpackInstallTask extends Task<Path> {
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
setName(i18n("modpack.installing"));
}
@Override
@@ -358,10 +358,10 @@ public final class ModpackHelper {
GameSettings.PROPERTY_MAX_MEMORY,
GameSettings.PROPERTY_PERM_SIZE
));
setting.autoMemoryProperty().setValue(effective.get(GameSettings::autoMemoryProperty));
setting.minMemoryProperty().setValue(effective.get(GameSettings::minMemoryProperty));
setting.autoMemoryProperty().setValue(effective.getInheritable(GameSettings::autoMemoryProperty));
setting.minMemoryProperty().setValue(effective.getInheritable(GameSettings::minMemoryProperty));
setting.maxMemoryProperty().setValue(manifest.getLaunchInfo().getMinMemory());
setting.permSizeProperty().setValue(effective.get(GameSettings::permSizeProperty));
setting.permSizeProperty().setValue(effective.getInheritable(GameSettings::permSizeProperty));
}
});
}
@@ -45,7 +45,10 @@ import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -65,21 +68,10 @@ public final class TexturesLoader {
}
// ==== Texture Loading ====
public static class LoadedTexture {
private final Image image;
private final Map<String, String> metadata;
public LoadedTexture(Image image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public Image getImage() {
return image;
}
public Map<String, String> getMetadata() {
return metadata;
public record LoadedTexture(Image image, Map<String, String> metadata) {
public LoadedTexture {
requireNonNull(image);
requireNonNull(metadata);
}
}
@@ -158,7 +150,7 @@ public final class TexturesLoader {
}
public static TextureModel getDefaultModel(UUID uuid) {
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).metadata().get("model"))
? TextureModel.WIDE
: TextureModel.SLIM;
}
@@ -168,7 +168,7 @@ public final class HMCLJavaRepository implements JavaRepository {
update.put("component", gameJavaVersion.component());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles().getFiles().forEach((path, file) -> {
result.remoteFiles().files().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
@@ -190,12 +190,12 @@ public final class HMCLJavaRepository implements JavaRepository {
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
Path javaDir = getJavaDir(platform, name);
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
if (!result.getInfo().getPlatform().equals(platform))
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
if (!result.info().getPlatform().equals(platform))
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.info().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
return JavaRuntime.of(executable, result.getInfo(), true);
return JavaRuntime.of(executable, result.info(), true);
});
}
@@ -34,33 +34,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
* @author Glavo
*/
@JsonAdapter(JavaManifest.Serializer.class)
public final class JavaManifest {
private final JavaInfo info;
@Nullable
private final Map<String, Object> update;
@Nullable
private final Map<String, JavaLocalFiles.Local> files;
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
this.info = info;
this.update = update;
this.files = files;
}
public JavaInfo getInfo() {
return info;
}
public Map<String, Object> getUpdate() {
return update;
}
public Map<String, JavaLocalFiles.Local> getFiles() {
return files;
}
public record JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
@@ -69,16 +43,16 @@ public final class JavaManifest {
@Override
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject res = new JsonObject();
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.getInfo().getVersion());
res.addProperty("java.vendor", src.getInfo().getVendor());
res.addProperty("os.name", src.info().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.info().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.info().getVersion());
res.addProperty("java.vendor", src.info().getVendor());
if (src.getUpdate() != null)
res.add("update", context.serialize(src.getUpdate()));
if (src.update() != null)
res.add("update", context.serialize(src.update()));
if (src.getFiles() != null)
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
if (src.files() != null)
res.add("files", context.serialize(src.files(), LOCAL_FILES_TYPE));
return res;
}
@@ -39,6 +39,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.SettingsManager.*;
@@ -54,6 +55,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@NotNullByDefault
public final class GameDirectoryManager {
/// The stable ID shared by newly created and migrated current-workspace game directories.
static final GameDirectoryID DEFAULT_GAME_DIRECTORY_ID =
new GameDirectoryID(new UUID(0x7105bc1f490e5e8cL, 0x878cf5844c3d4bc3L));
/// The stable ID shared by newly created and migrated user-home game directories.
static final GameDirectoryID HOME_GAME_DIRECTORY_ID =
new GameDirectoryID(new UUID(0xf3eafde8506e5a77L, 0xbc88f24b4728dfb2L));
/// The default current-workspace game directory path.
private static final PortablePath CURRENT_GAME_DIRECTORY_PATH = PortablePath.of(".minecraft");
@@ -154,12 +163,14 @@ public final class GameDirectoryManager {
if (localGameDirectories().isNewlyCreated() && localGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = localGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
gameDirectories.getGameDirectories().add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
}
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = userGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
gameDirectories.getGameDirectories().add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
}
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
@@ -216,9 +227,11 @@ public final class GameDirectoryManager {
if (localGameDirectories().getGameDirectories().isEmpty()
&& userGameDirectories().getGameDirectories().isEmpty()) {
localGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
.add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
userGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
.add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
return true;
} else {
return false;
@@ -67,7 +67,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
public sealed abstract class GameSettings extends ObservableSetting {
/// Suggested maximum heap memory in MiB.
static final int SUGGESTED_MEMORY;
public static final int SUGGESTED_MEMORY;
static {
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
@@ -331,6 +331,11 @@ public sealed abstract class GameSettings extends ObservableSetting {
}
/// Creates a new inheritable property.
protected final <T extends @Nullable Object> InheritableProperty<@Nullable T> newInheritableProperty(String name) {
return new SimpleInheritableProperty<>(this, name, null);
}
/// Creates a new inheritable property with the given default value.
protected final <T extends @UnknownNullability Object> InheritableProperty<T> newInheritableProperty(String name, T defaultValue) {
return new SimpleInheritableProperty<>(this, name, defaultValue);
}
@@ -388,10 +393,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized JVM options.
@SerializedName(PROPERTY_JVM_OPTIONS)
private final SettingProperty<String> jvmOptions = newSettingProperty(PROPERTY_JVM_OPTIONS, "");
private final InheritableProperty<String> jvmOptions = newInheritableProperty(PROPERTY_JVM_OPTIONS, "");
/// Returns the user customized JVM options property.
public SettingProperty<String> jvmOptionsProperty() {
public InheritableProperty<String> jvmOptionsProperty() {
return jvmOptions;
}
@@ -448,10 +453,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will automatically adjust memory allocation.
@SerializedName(PROPERTY_AUTO_MEMORY)
private final SettingProperty<Boolean> autoMemory = newSettingProperty(PROPERTY_AUTO_MEMORY, true);
private final InheritableProperty<Boolean> autoMemory = newInheritableProperty(PROPERTY_AUTO_MEMORY, true);
/// Returns the automatic memory allocation property.
public SettingProperty<Boolean> autoMemoryProperty() {
public InheritableProperty<Boolean> autoMemoryProperty() {
return autoMemory;
}
@@ -460,10 +465,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The minimum heap memory in MiB.
@SerializedName(PROPERTY_MIN_MEMORY)
private final SettingProperty<@Nullable Integer> minMemory = newSettingProperty(PROPERTY_MIN_MEMORY);
private final InheritableProperty<@Nullable Integer> minMemory = newInheritableProperty(PROPERTY_MIN_MEMORY);
/// Returns the minimum heap memory property.
public SettingProperty<@Nullable Integer> minMemoryProperty() {
public InheritableProperty<@Nullable Integer> minMemoryProperty() {
return minMemory;
}
@@ -472,10 +477,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The maximum heap memory in MiB.
@SerializedName(PROPERTY_MAX_MEMORY)
private final SettingProperty<@Nullable Integer> maxMemory = newSettingProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
private final InheritableProperty<@Nullable Integer> maxMemory = newInheritableProperty(PROPERTY_MAX_MEMORY, SUGGESTED_MEMORY);
/// Returns the maximum heap memory property.
public SettingProperty<@Nullable Integer> maxMemoryProperty() {
public InheritableProperty<@Nullable Integer> maxMemoryProperty() {
return maxMemory;
}
@@ -484,10 +489,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The permanent generation or metaspace size in MiB.
@SerializedName(PROPERTY_PERM_SIZE)
private final SettingProperty<String> permSize = newSettingProperty(PROPERTY_PERM_SIZE, "");
private final InheritableProperty<String> permSize = newInheritableProperty(PROPERTY_PERM_SIZE, "");
/// Returns the permanent generation or metaspace size property.
public SettingProperty<String> permSizeProperty() {
public InheritableProperty<String> permSizeProperty() {
return permSize;
}
@@ -593,10 +598,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized arguments passed to the game.
@SerializedName(PROPERTY_GAME_ARGS)
private final SettingProperty<String> gameArguments = newSettingProperty(PROPERTY_GAME_ARGS, "");
private final InheritableProperty<String> gameArguments = newInheritableProperty(PROPERTY_GAME_ARGS, "");
/// Returns the customized game arguments property.
public SettingProperty<String> gameArgumentsProperty() {
public InheritableProperty<String> gameArgumentsProperty() {
return gameArguments;
}
@@ -643,10 +648,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// User customized environment variables passed to the game.
@SerializedName(PROPERTY_ENVIRONMENT_VARIABLES)
private final SettingProperty<String> environmentVariables = newSettingProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
private final InheritableProperty<String> environmentVariables = newInheritableProperty(PROPERTY_ENVIRONMENT_VARIABLES, "");
/// Returns the customized environment variables property.
public SettingProperty<String> environmentVariablesProperty() {
public InheritableProperty<String> environmentVariablesProperty() {
return environmentVariables;
}
@@ -763,10 +768,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL does not patch native libraries.
@SerializedName(PROPERTY_NOT_PATCH_NATIVES)
private final SettingProperty<Boolean> notPatchNatives = newSettingProperty(PROPERTY_NOT_PATCH_NATIVES, false);
private final InheritableProperty<Boolean> notPatchNatives = newInheritableProperty(PROPERTY_NOT_PATCH_NATIVES, false);
/// Returns the native library patching property.
public SettingProperty<Boolean> notPatchNativesProperty() {
public InheritableProperty<Boolean> notPatchNativesProperty() {
return notPatchNatives;
}
@@ -775,10 +780,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL leaves native library management to the user.
@SerializedName(PROPERTY_USE_CUSTOM_NATIVES)
private final SettingProperty<Boolean> useCustomNatives = newSettingProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
private final InheritableProperty<Boolean> useCustomNatives = newInheritableProperty(PROPERTY_USE_CUSTOM_NATIVES, false);
/// Returns the custom native libraries property.
public SettingProperty<Boolean> useCustomNativesProperty() {
public InheritableProperty<Boolean> useCustomNativesProperty() {
return useCustomNatives;
}
@@ -787,10 +792,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// The path to the native library directory, or an empty string for the default path.
@SerializedName(PROPERTY_NATIVES_DIRECTORY)
private final SettingProperty<String> nativesDirectory = newSettingProperty(PROPERTY_NATIVES_DIRECTORY, "");
private final InheritableProperty<String> nativesDirectory = newInheritableProperty(PROPERTY_NATIVES_DIRECTORY, "");
/// Returns the native library directory property.
public SettingProperty<String> nativesDirectoryProperty() {
public InheritableProperty<String> nativesDirectoryProperty() {
return nativesDirectory;
}
@@ -799,10 +804,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will use native GLFW.
@SerializedName(PROPERTY_USE_NATIVE_GLFW)
private final SettingProperty<Boolean> useNativeGLFW = newSettingProperty(PROPERTY_USE_NATIVE_GLFW, false);
private final InheritableProperty<Boolean> useNativeGLFW = newInheritableProperty(PROPERTY_USE_NATIVE_GLFW, false);
/// Returns the native GLFW property.
public SettingProperty<Boolean> useNativeGLFWProperty() {
public InheritableProperty<Boolean> useNativeGLFWProperty() {
return useNativeGLFW;
}
@@ -811,10 +816,10 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// If `true`, HMCL will use native OpenAL.
@SerializedName(PROPERTY_USE_NATIVE_OPENAL)
private final SettingProperty<Boolean> useNativeOpenAL = newSettingProperty(PROPERTY_USE_NATIVE_OPENAL, false);
private final InheritableProperty<Boolean> useNativeOpenAL = newInheritableProperty(PROPERTY_USE_NATIVE_OPENAL, false);
/// Returns the native OpenAL property.
public SettingProperty<Boolean> useNativeOpenALProperty() {
public InheritableProperty<Boolean> useNativeOpenALProperty() {
return useNativeOpenAL;
}
@@ -898,11 +903,6 @@ public sealed abstract class GameSettings extends ObservableSetting {
return instance;
}
/// Returns the effective value for a property whose override state is tracked by the property itself.
public <T extends @UnknownNullability Object> T get(Function<GameSettings, SettingProperty<T>> propertyGetter) {
return inherited(preset, instance, propertyGetter);
}
/// Returns the effective value for an inheritable property.
public <T extends @UnknownNullability Object> T getInheritable(
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
@@ -981,7 +981,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
/// Returns the effective maximum heap memory in MiB.
public int getMaxMemory() {
Integer value = get(GameSettings::maxMemoryProperty);
Integer value = getInheritable(GameSettings::maxMemoryProperty);
return value != null && value > 0 ? value : SUGGESTED_MEMORY;
}
@@ -996,11 +996,16 @@ public sealed abstract class GameSettings extends ObservableSetting {
}
/// Returns the effective renderer.
public Renderer getRenderer() {
return switch (getInheritable(GameSettings::graphicsBackendProperty)) {
public Renderer getRenderer(GameVersionNumber version) {
GraphicsAPI graphicsAPI = getInheritable(GameSettings::graphicsBackendProperty);
if (graphicsAPI == GraphicsAPI.DEFAULT) {
graphicsAPI = GraphicsAPI.getDefaultGraphicsAPI(version);
}
return switch (graphicsAPI) {
case DEFAULT -> Renderer.DEFAULT;
case OPENGL -> selectRenderer(GraphicsAPI.OPENGL, getInheritable(GameSettings::openGLRendererProperty));
case VULKAN -> selectRenderer(GraphicsAPI.VULKAN, getInheritable(GameSettings::vulkanRendererProperty));
case DEFAULT -> Renderer.DEFAULT;
};
}
@@ -129,6 +129,21 @@ public final class GameSettingsPresets extends ObservableSetting implements Json
return id;
}
/// Returns the first automatic preset number that is not used by existing presets.
public int newPresetAutoNameNumber() {
mainLoop:
for (int index = 1; ; index++) {
for (GameSettings.Preset setting : getPresets()) {
Integer autoNameNumber = setting.autoNameNumberProperty().getValue();
if ((autoNameNumber != null && autoNameNumber == index)) {
continue mainLoop;
}
}
return index;
}
}
/// Returns the preset with the given ID.
public GameSettings.@Nullable Preset getPreset(@Nullable GameSettingsPresetID id) {
if (id == null) {
@@ -87,6 +87,9 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Theme appearance override key for title-bar transparency.
public static final String THEME_APPEARANCE_TITLE_BAR_TRANSPARENT = "titleBarTransparent";
/// Theme appearance override key for window transparency.
public static final String THEME_APPEARANCE_WINDOW_TRANSPARENT = "windowTransparent";
/// Theme appearance override key for the primary background source.
public static final String THEME_APPEARANCE_BACKGROUND = "background";
@@ -336,6 +339,15 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
return titleBarTransparent;
}
/// Whether translucent launcher backgrounds reveal content behind the window.
@SerializedName("windowTransparent")
private final BooleanProperty windowTransparent = new SimpleBooleanProperty(false);
/// Returns the window transparency property.
public BooleanProperty windowTransparentProperty() {
return windowTransparent;
}
// Background source
/// The launcher background source type.
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonSchema;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
@@ -80,12 +81,6 @@ public final class LegacyConfigMigrator {
/// The legacy built-in profile name for the user-home game directory.
private static final String LEGACY_HOME_PROFILE = "Home";
/// The legacy built-in current-workspace profile ID.
private static final GameDirectoryID LEGACY_DEFAULT_PROFILE_ID = getLegacyProfileID(LEGACY_DEFAULT_PROFILE);
/// The legacy built-in user-home profile ID.
private static final GameDirectoryID LEGACY_HOME_PROFILE_ID = getLegacyProfileID(LEGACY_HOME_PROFILE);
/// The legacy Windows and portable configuration file name used before HMCL 3.16.
private static final String LEGACY_CONFIG_FILENAME = "hmcl.json";
@@ -260,6 +255,10 @@ public final class LegacyConfigMigrator {
renameMember(jsonObject, "bgpaint", "customBackgroundPaint");
renameMember(jsonObject, "backgroundPaint", "customBackgroundPaint");
migrateBackgroundOpacity(jsonObject, themeAppearanceOverrides);
jsonObject.addProperty("windowTransparent", true);
addThemeAppearanceOverride(
themeAppearanceOverrides,
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT);
writeThemeAppearanceOverrides(jsonObject, themeAppearanceOverrides);
renameMember(jsonObject, "proxyUserName", "proxyUser");
migrateLegacySelectedVersions(jsonObject);
@@ -276,6 +275,7 @@ public final class LegacyConfigMigrator {
GameSettingsPresets gameSettingsPresets = new GameSettingsPresets();
migrateLegacyPresetSettings(gameDirectories, gameSettingsPresets, legacyConfigurations);
migrateLegacyInstanceGameSettings(gameDirectories, gameSettingsPresets);
migrateLegacyAllowAutoAgent(deserialized, gameSettingsPresets, legacyAllowAutoAgent);
migrateLegacyDisableAutoGameOptions(deserialized, gameSettingsPresets, legacyDisableAutoGameOptions);
DetachedSettings detachedSettings = new DetachedSettings(gameDirectories, gameSettingsPresets,
@@ -1272,18 +1272,95 @@ public final class LegacyConfigMigrator {
continue;
}
legacyParent = LegacyGameSettingsMigrator.toPreset(legacyGameSettings, profileName, legacySettingObject);
legacyParent = LegacyGameSettingsMigrator.toPreset(legacyGameSettings, gameSettingsPresets.newPresetAutoNameNumber(), legacySettingObject);
gameSettingsPresets.getPresets().add(legacyParent);
}
}
}
/// Migrates existing instances under migrated legacy game directories to explicit instance settings.
static void migrateLegacyInstanceGameSettings(
GameDirectories gameDirectories,
GameSettingsPresets gameSettingsPresets) {
Objects.requireNonNull(gameDirectories);
Objects.requireNonNull(gameSettingsPresets);
for (GameDirectory gameDirectory : gameDirectories.getGameDirectories()) {
@Nullable GameSettingsPresetID legacyGameSettings = gameDirectory.getLegacyGameSettings();
if (legacyGameSettings == null) {
continue;
}
GameSettings.Preset legacyParent = gameSettingsPresets.getPreset(legacyGameSettings);
if (legacyParent == null) {
continue;
}
migrateLegacyInstanceGameSettings(gameDirectory.getPath().toPath(), legacyGameSettings, legacyParent);
}
}
/// Migrates existing instances under one game directory to explicit instance settings.
private static void migrateLegacyInstanceGameSettings(
Path gameDirectory,
GameSettingsPresetID legacyGameSettings,
GameSettings.Preset legacyParent) {
Path versionsDirectory = gameDirectory.resolve("versions");
if (!Files.isDirectory(versionsDirectory)) {
return;
}
try (var stream = Files.list(versionsDirectory)) {
stream.filter(Files::isDirectory)
.forEach(instanceRoot -> migrateLegacyInstanceGameSettingsInVersionRoot(
instanceRoot,
legacyGameSettings,
legacyParent));
} catch (IOException e) {
LOG.warning("Failed to migrate legacy instance game settings under " + versionsDirectory, e);
}
}
/// Migrates one existing instance to explicit instance settings.
private static void migrateLegacyInstanceGameSettingsInVersionRoot(
Path instanceRoot,
GameSettingsPresetID legacyGameSettings,
GameSettings.Preset legacyParent) {
@Nullable Path instanceId = instanceRoot.getFileName();
if (instanceId == null || !Files.isRegularFile(instanceRoot.resolve(instanceId.toString() + ".json"))) {
return;
}
Path currentSettings = instanceRoot.resolve(LegacyGameSettingsMigrator.INSTANCE_METADATA_DIRECTORY)
.resolve(LegacyGameSettingsMigrator.INSTANCE_CONFIG_DIRECTORY)
.resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME);
if (Files.exists(currentSettings)) {
return;
}
if (Files.exists(instanceRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME))) {
return;
}
GameSettings.Instance setting = new GameSettings.Instance();
setting.parentProperty().setValue(legacyGameSettings);
if (legacyParent.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS
&& StringUtils.isBlank(legacyParent.runningDirectoryProperty().getValue())) {
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
}
try {
FileUtils.saveSafely(currentSettings, LauncherSettings.SETTINGS_GSON.toJson(setting));
} catch (IOException e) {
LOG.warning("Failed to save migrated instance game settings " + currentSettings, e);
}
}
/// Returns the legacy profile name used in `configurations`.
private static @Nullable String getLegacyProfileName(GameDirectory gameDirectory) {
if (LEGACY_DEFAULT_PROFILE_ID.equals(gameDirectory.getId())) {
if (GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
return LEGACY_DEFAULT_PROFILE;
}
if (LEGACY_HOME_PROFILE_ID.equals(gameDirectory.getId())) {
if (GameDirectoryManager.HOME_GAME_DIRECTORY_ID.equals(gameDirectory.getId())) {
return LEGACY_HOME_PROFILE;
}
return GameDirectoryManager.getGameDirectoryCustomName(gameDirectory);
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.setting.property.InheritableProperty;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
@@ -43,12 +42,24 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
public final class LegacyGameSettingsMigrator {
/// Legacy file name used by old per-version `VersionSetting` data.
private static final String LEGACY_INSTANCE_SETTINGS_FILENAME = "hmclversion.cfg";
static final String LEGACY_INSTANCE_SETTINGS_FILENAME = "hmclversion.cfg";
/// Receipt file name used to record successful legacy per-version settings migration.
private static final String LEGACY_INSTANCE_SETTINGS_MIGRATION_RECEIPT_FILENAME =
"instance-game-settings.migration-receipt.json";
/// Directory under the version root that stores HMCL-managed instance metadata.
static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
/// Directory under the instance metadata directory that stores instance configuration files.
static final String INSTANCE_CONFIG_DIRECTORY = "config";
/// Directory under the instance metadata directory that stores instance state files.
private static final String INSTANCE_STATE_DIRECTORY = "state";
/// Current file name for instance-specific game settings.
static final String INSTANCE_GAME_SETTINGS_FILENAME = "instance-game-settings.json";
/// Legacy game directory modes stored by old configuration files.
private enum GameDirectoryType {
/// Use the root `.minecraft` folder.
@@ -101,9 +112,9 @@ public final class LegacyGameSettingsMigrator {
}
/// Converts a legacy profile-level setting JSON object into a preset with the given ID.
public static GameSettings.Preset toPreset(GameSettingsPresetID id, String name, @Nullable JsonObject source) {
public static GameSettings.Preset toPreset(GameSettingsPresetID id, int autoNameNumber, @Nullable JsonObject source) {
GameSettings.Preset target = new GameSettings.Preset(id);
target.nameProperty().setValue(LocalizedText.plain(name));
target.autoNameNumberProperty().setValue(autoNameNumber);
if (getLegacyGameDirType(source, GameDirectoryType.ROOT_FOLDER) == GameDirectoryType.VERSION_FOLDER) {
target.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
}
@@ -25,6 +25,9 @@ public enum ThemeColorType {
/// Uses the built-in launcher default theme color.
DEFAULT,
/// Follows the accent color provided by the operating system.
SYSTEM,
/// Uses the custom launcher theme color selected by the user.
CUSTOM,
@@ -47,6 +47,7 @@ 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;
@@ -0,0 +1,39 @@
/*
* 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.theme;
import javafx.scene.layout.Background;
import org.jetbrains.annotations.NotNullByDefault;
import java.util.Objects;
/// A launcher background and the opacity applied by its rendering node.
///
/// @param background the JavaFX background rendered by the launcher
/// @param opacity the node opacity in the range `[0, 1]`
@NotNullByDefault
public record LauncherBackground(Background background, double opacity) {
/// Creates a launcher background value.
public LauncherBackground {
Objects.requireNonNull(background);
if (!Double.isFinite(opacity) || opacity < 0.0 || opacity > 1.0) {
throw new IllegalArgumentException("Launcher background opacity must be between 0 and 1: " + opacity);
}
}
}
@@ -43,6 +43,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @param contrast the MonetFX contrast level, or `null` when inherited
/// @param background the background settings, or `null` when inherited
/// @param titleBar the title-bar settings, or `null` when inherited
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
@NotNullByDefault
public record ThemeAppearance(
@Nullable ThemeColorSource color,
@@ -50,7 +51,8 @@ public record ThemeAppearance(
@Nullable ColorStyle colorStyle,
@Nullable Contrast contrast,
@Nullable ThemeBackgroundSettings background,
@Nullable ThemeTitleBar titleBar) {
@Nullable ThemeTitleBar titleBar,
@Nullable Boolean windowTransparent) {
/// JSON member name for the color seed.
static final String FIELD_COLOR = "color";
@@ -70,6 +72,9 @@ public record ThemeAppearance(
/// JSON member name for title-bar settings.
static final String FIELD_TITLE_BAR = "titleBar";
/// JSON member name for window transparency.
static final String FIELD_WINDOW_TRANSPARENT = "windowTransparent";
/// Creates an appearance patch.
///
/// @param color the color seed source, or `null` when inherited
@@ -78,6 +83,7 @@ public record ThemeAppearance(
/// @param contrast the MonetFX contrast level, or `null` when inherited
/// @param background the background settings, or `null` when inherited
/// @param titleBar the title-bar settings, or `null` when inherited
/// @param windowTransparent whether the launcher window should be transparent, or `null` when inherited
public ThemeAppearance {
if (background != null && background.isEmpty()) {
background = null;
@@ -100,7 +106,8 @@ public record ThemeAppearance(
readColorStyle(object),
readContrast(object),
readBackground(object),
readTitleBar(object));
readTitleBar(object),
readWindowTransparent(object));
}
/// Returns whether this appearance contains no concrete fields.
@@ -112,7 +119,8 @@ public record ThemeAppearance(
&& colorStyle == null
&& contrast == null
&& background == null
&& titleBar == null;
&& titleBar == null
&& windowTransparent == null;
}
/// Adds this appearance patch's concrete fields to a JSON object.
@@ -139,6 +147,9 @@ public record ThemeAppearance(
if (titleBar != null) {
object.add(FIELD_TITLE_BAR, titleBar.toJsonObject());
}
if (windowTransparent != null) {
object.addProperty(FIELD_WINDOW_TRANSPARENT, windowTransparent);
}
}
/// Converts this appearance patch to its JSON representation.
@@ -167,7 +178,8 @@ public record ThemeAppearance(
: patch.background != null ? patch.background : background,
titleBar != null && patch.titleBar != null
? titleBar.merge(patch.titleBar)
: patch.titleBar != null ? patch.titleBar : titleBar);
: patch.titleBar != null ? patch.titleBar : titleBar,
patch.windowTransparent != null ? patch.windowTransparent : windowTransparent);
}
/// Converts this appearance to concrete launcher theme values.
@@ -309,6 +321,19 @@ public record ThemeAppearance(
}
}
/// Reads the optional window transparency field.
private static @Nullable Boolean readWindowTransparent(JsonObject object) {
JsonElement element = object.get(FIELD_WINDOW_TRANSPARENT);
if (element == null) {
return null;
}
if (!(element instanceof JsonPrimitive primitive) || !primitive.isBoolean()) {
LOG.warning("Ignored invalid theme windowTransparent: expected a boolean, got " + element);
return null;
}
return primitive.getAsBoolean();
}
/// Reads an optional string field.
private static @Nullable String readString(JsonObject object, String field) {
JsonElement element = object.get(field);
@@ -77,7 +77,7 @@ public final class ThemePackManager {
private static final Path USER_THEME_PACKS_DIRECTORY = Metadata.HMCL_USER_HOME.resolve("themes");
/// Default version used when exporting the current launcher appearance.
private static final String CURRENT_THEME_PACK_VERSION = "1.0.0";
public static final String CURRENT_THEME_PACK_VERSION = "1.0.0";
/// IDs of launcher-bundled theme packs in display order.
private static final @Unmodifiable List<String> BUILTIN_THEME_PACK_IDS = List.of(
@@ -615,13 +615,14 @@ public final class ThemePackManager {
///
/// @param outputFile the target theme-pack file
/// @param packId the exported package identifier
/// @param version the exported package version
/// @param packName the exported package display name
/// @param authorName the exported package author name
/// @throws IOException if the current appearance cannot be exported
public static void exportCurrent(Path outputFile, String packId, String packName, String authorName) throws IOException {
public static void exportCurrent(Path outputFile, String packId, String version, String packName, String authorName) throws IOException {
Objects.requireNonNull(outputFile);
ExportedThemePack themePack = createCurrent(packId, packName, authorName);
ExportedThemePack themePack = createCurrent(packId, version, packName, authorName);
try {
ThemePackExporter.export(themePack.manifest(), themePack.assets(), outputFile);
} finally {
@@ -638,12 +639,14 @@ public final class ThemePackManager {
/// Creates an exportable theme pack from the current launcher appearance.
///
/// @param packId the exported package identifier
/// @param version the exported package version
/// @param packName the exported package display name
/// @param authorName the exported package author name
/// @return the exportable theme-pack descriptor
/// @throws IOException if the current appearance cannot be represented as a theme pack
public static ExportedThemePack createCurrent(String packId, String packName, String authorName) throws IOException {
public static ExportedThemePack createCurrent(String packId, String version, String packName, String authorName) throws IOException {
packId = ThemePackManifest.requirePackageId(packId);
version = requireNonBlank(version, "version");
packName = requireNonBlank(packName, "packName");
authorName = requireNonBlank(authorName, "authorName");
@@ -657,7 +660,8 @@ public final class ThemePackManager {
currentColorStyle(),
null,
background,
new ThemeTitleBar(currentTitleBarTransparent()));
new ThemeTitleBar(currentTitleBarTransparent()),
currentWindowTransparent());
Theme theme = new Theme(
null,
null,
@@ -668,7 +672,7 @@ public final class ThemePackManager {
List.of());
ThemePackManifest manifest = new ThemePackManifest(
packId,
CURRENT_THEME_PACK_VERSION,
version,
LocalizedText.plain(packName),
List.of(new ThemePackAuthor(LocalizedText.plain(authorName))),
background.source() instanceof ThemeBackground.Image image ? image.path() : null,
@@ -708,6 +712,9 @@ public final class ThemePackManager {
if (themeColorType == ThemeColorType.DEFAULT) {
return ThemeColorSource.DEFAULT;
}
if (themeColorType == ThemeColorType.SYSTEM) {
return ThemeColorSource.custom(Themes.getSystemThemeColor());
}
if (themeColorType == ThemeColorType.BACKGROUND) {
if (backgroundType == BackgroundType.THEME_COLOR) {
return ThemeColorSource.DEFAULT;
@@ -752,6 +759,13 @@ public final class ThemePackManager {
: Objects.requireNonNullElse(resolveCurrentTitleBarTransparent(currentResolveContext()), false);
}
/// Returns whether the current launcher window should be transparent.
private static boolean currentWindowTransparent() throws IOException {
return SettingsManager.settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)
? settings().windowTransparentProperty().get()
: Objects.requireNonNullElse(resolveCurrentWindowTransparent(currentResolveContext()), false);
}
/// Resolves the selected theme's controlled brightness.
///
/// @param context the condition resolution context
@@ -792,6 +806,16 @@ public final class ThemePackManager {
return appearance != null && appearance.titleBar() != null ? appearance.titleBar().transparent() : null;
}
/// Resolves the selected theme window transparency directive.
///
/// @param context the condition resolution context
/// @return the selected theme window transparency directive, or `null` when unavailable
/// @throws IOException if the selected theme pack cannot be read
public static @Nullable Boolean resolveCurrentWindowTransparent(ThemeResolveContext context) throws IOException {
@Nullable ThemeAppearance appearance = resolveCurrentThemeAppearance(context);
return appearance != null ? appearance.windowTransparent() : null;
}
/// Resolves the current selected theme color source without extracting any wallpaper pixels.
///
/// @param context the condition resolution context
@@ -32,9 +32,6 @@ import javafx.collections.SetChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BackgroundImage;
@@ -115,6 +112,9 @@ public final class Themes {
if (FXUtils.DARK_MODE != null) {
observables.add(FXUtils.DARK_MODE);
}
if (FXUtils.ACCENT_COLOR != null) {
observables.add(FXUtils.ACCENT_COLOR);
}
bind(observables.toArray(new Observable[0]));
}
@@ -195,6 +195,7 @@ public final class Themes {
}
return switch (themeColorType) {
case DEFAULT -> ThemeColor.DEFAULT;
case SYSTEM -> getSystemThemeColor();
case CUSTOM -> fallback;
case BACKGROUND -> resolveWallpaperThemeColor(fallback, backgroundType);
};
@@ -282,9 +283,11 @@ public final class Themes {
colorScheme
);
/// The current JavaFX launcher background.
private static final ReadOnlyObjectWrapper<Background> background = new ReadOnlyObjectWrapper<>(
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
/// The current JavaFX launcher background and its node opacity.
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
new LauncherBackground(
new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)),
1.0));
/// The last loaded background source used to rebuild the JavaFX background without reloading images.
private static @Nullable LoadedBackground loadedBackground;
@@ -300,8 +303,8 @@ public final class Themes {
/// A loaded JavaFX background source and the wallpaper color extracted while loading it.
private sealed interface LoadedBackground {
/// Returns the loaded JavaFX background.
Background background();
/// Returns the loaded JavaFX background and its node opacity.
LauncherBackground background();
/// Returns whether this background is the configured fallback background.
boolean fallbackBackground();
@@ -316,7 +319,7 @@ public final class Themes {
/// @param wallpaperThemeColor the seed color extracted from [#image], or `null` when unavailable
/// @param fallbackBackground whether this background is the configured fallback background
record Image(
Background background,
LauncherBackground background,
javafx.scene.image.Image image,
@Nullable ThemeColor wallpaperThemeColor,
boolean fallbackBackground) implements LoadedBackground {
@@ -339,7 +342,7 @@ public final class Themes {
/// @param wallpaperThemeColor the seed color represented by [#paint], or `null` when unavailable
/// @param fallbackBackground whether this background is the configured fallback background
record Paint(
Background background,
LauncherBackground background,
@Nullable javafx.scene.paint.Paint paint,
@Nullable ThemeColor wallpaperThemeColor,
boolean fallbackBackground) implements LoadedBackground {
@@ -359,7 +362,7 @@ public final class Themes {
/// @param background the loaded JavaFX background
/// @param fallbackBackground whether this background is the configured fallback background
record ThemeColorFill(
Background background,
LauncherBackground background,
boolean fallbackBackground) implements LoadedBackground {
/// Creates a loaded theme-color background.
///
@@ -528,6 +531,15 @@ public final class Themes {
return defaultBrightness = brightness;
}
/// Returns the current operating system accent color, or the launcher default when unavailable.
public static ThemeColor getSystemThemeColor() {
if (FXUtils.ACCENT_COLOR == null) {
return ThemeColor.DEFAULT;
}
@Nullable Color accentColor = FXUtils.ACCENT_COLOR.get();
return accentColor != null ? ThemeColor.of(accentColor) : ThemeColor.DEFAULT;
}
/// Returns the current resolved launcher theme.
public static ResolvedTheme getTheme() {
return theme.get();
@@ -543,8 +555,8 @@ public final class Themes {
return colorScheme.get();
}
/// Returns the current JavaFX launcher background property.
public static ReadOnlyObjectProperty<Background> backgroundProperty() {
/// Returns the current JavaFX launcher background and node opacity property.
public static ReadOnlyObjectProperty<LauncherBackground> backgroundProperty() {
startBackgroundUpdates();
return background.getReadOnlyProperty();
}
@@ -824,7 +836,8 @@ public final class Themes {
}
@Nullable LoadedBackground loaded = loadedBackground;
if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
Background newBackground = createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
LauncherBackground newBackground =
createThemeColorBackground(getLoadedBackgroundOpacity(loaded.fallbackBackground()));
loadedBackground = new LoadedBackground.ThemeColorFill(newBackground, themeColorFill.fallbackBackground());
background.set(newBackground);
}
@@ -858,17 +871,23 @@ public final class Themes {
LoadedBackground refreshed;
if (loaded instanceof LoadedBackground.Image imageBackground) {
refreshed = new LoadedBackground.Image(
createBackgroundWithOpacity(imageBackground.image(), opacity),
new LauncherBackground(
imageBackground.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
imageBackground.image(),
imageBackground.wallpaperThemeColor(),
imageBackground.fallbackBackground());
} else if (loaded instanceof LoadedBackground.ThemeColorFill themeColorFill) {
refreshed = new LoadedBackground.ThemeColorFill(
createThemeColorBackground(opacity),
new LauncherBackground(
themeColorFill.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
themeColorFill.fallbackBackground());
} else if (loaded instanceof LoadedBackground.Paint paintBackground) {
refreshed = new LoadedBackground.Paint(
createPaintBackground(paintBackground.paint(), opacity),
new LauncherBackground(
paintBackground.background().background(),
MathUtils.clamp(opacity, 0., 1.)),
paintBackground.paint(),
paintBackground.wallpaperThemeColor(),
paintBackground.fallbackBackground());
@@ -917,60 +936,37 @@ public final class Themes {
return FXUtils.loadImage(new CacheFileTask(url).run());
}
/// Creates a JavaFX paint background with the requested opacity.
private static Background createPaintBackground(@Nullable Paint paint, double opacity) {
opacity = MathUtils.clamp(opacity, 0., 1.);
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null) {
color = Color.WHITE;
}
if (opacity < 1.) {
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
}
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
} else {
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
}
/// Creates a JavaFX paint background with a separately stored node opacity.
private static LauncherBackground createPaintBackground(@Nullable Paint paint, double opacity) {
return new LauncherBackground(
new Background(new BackgroundFill(
Objects.requireNonNullElse(paint, Color.WHITE),
CornerRadii.EMPTY,
Insets.EMPTY)),
MathUtils.clamp(opacity, 0., 1.));
}
/// Creates a JavaFX background from the current theme color scheme surface container.
private static Background createThemeColorBackground(double opacity) {
private static LauncherBackground createThemeColorBackground(double opacity) {
return createPaintBackground(getColorScheme().getColor(ColorRole.SURFACE_CONTAINER), opacity);
}
/// Creates a JavaFX image background with the requested opacity.
private static Background createBackgroundWithOpacity(Image image, double opacity) {
PixelReader pixelReader = image.getPixelReader();
if (opacity <= 0) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
} else if (opacity >= 1. || pixelReader == null) {
return new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
} else {
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
PixelWriter pixelWriter = tempImage.getPixelWriter();
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
Color color = pixelReader.getColor(x, y);
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity);
pixelWriter.setColor(x, y, newColor);
}
}
return new Background(new BackgroundImage(
tempImage,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(800, 480, false, false, true, true)
));
}
/// Creates a JavaFX image background with a separately stored node opacity.
private static LauncherBackground createBackgroundWithOpacity(Image image, double opacity) {
Background imageBackground = new Background(new BackgroundImage(
image,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.DEFAULT,
new BackgroundSize(
BackgroundSize.AUTO,
BackgroundSize.AUTO,
false,
false,
false,
true)
));
return new LauncherBackground(imageBackground, MathUtils.clamp(opacity, 0., 1.));
}
/// Loads a default launcher background image from local workspace files.
@@ -1073,6 +1069,27 @@ public final class Themes {
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
);
/// Whether the launcher window should be transparent after applying launcher and theme settings.
private static final BooleanBinding windowTransparent = Bindings.createBooleanBinding(
() -> {
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT)) {
return settings().windowTransparentProperty().get();
}
try {
return Objects.requireNonNullElse(
ThemePackManager.resolveCurrentWindowTransparent(ThemePackManager.currentResolveContext()),
false);
} catch (IOException | RuntimeException e) {
return false;
}
},
settings().windowTransparentProperty(),
settings().getThemeAppearanceOverrides(),
settings().selectedThemeProperty(),
settings().themeBrightnessModeProperty(),
FXUtils.DARK_MODE != null ? FXUtils.DARK_MODE : settings().themeBrightnessModeProperty()
);
/// The title text fill derived from the current color scheme.
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
() -> titleBarTransparent.get()
@@ -1092,6 +1109,11 @@ public final class Themes {
return titleBarTransparent;
}
/// Returns whether the launcher window should be transparent after applying launcher and theme settings.
public static BooleanBinding windowTransparentProperty() {
return windowTransparent;
}
/// Returns whether the current color scheme uses dark brightness.
public static BooleanBinding darkModeProperty() {
return darkMode;
@@ -92,11 +92,7 @@ import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
@@ -1691,6 +1687,7 @@ public final class FXUtils {
control.setOnContextMenuRequested(e -> {
boolean hasNoSelection = control.getSelectedText().isEmpty();
boolean isPassword = control instanceof PasswordField;
IconedMenuItem undo = new IconedMenuItem(SVG.UNDO, i18n("menu.undo"), control::undo, popup);
IconedMenuItem redo = new IconedMenuItem(SVG.REDO, i18n("menu.redo"), control::redo, popup);
@@ -1704,9 +1701,9 @@ public final class FXUtils {
undo.setDisable(!control.isUndoable());
redo.setDisable(!control.isRedoable());
cut.setDisable(hasNoSelection);
cut.setDisable(hasNoSelection || isPassword);
delete.setDisable(hasNoSelection);
copy.setDisable(hasNoSelection);
copy.setDisable(hasNoSelection || isPassword);
paste.setDisable(!Clipboard.getSystemClipboard().hasString());
selectall.setDisable(control.getText() == null || control.getText().isEmpty());
@@ -167,7 +167,7 @@ public class GameCrashWindow extends Stage {
Set<String> keywords = new HashSet<>();
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
for (CrashReportAnalyzer.Result result : pair.getKey()) {
results.put(result.getRule(), result);
results.put(result.rule(), result);
}
keywords.addAll(pair.getValue());
}
@@ -184,22 +184,22 @@ public class GameCrashWindow extends Stage {
for (CrashReportAnalyzer.Result result : results.values()) {
String message;
switch (result.getRule()) {
switch (result.rule()) {
case TOO_OLD_JAVA:
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.matcher().group("expected"))));
break;
case MOD_RESOLUTION_CONFLICT:
case MOD_RESOLUTION_MISSING:
case MOD_RESOLUTION_COLLECTION:
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("sourcemod")),
parseFabricModId(result.getMatcher().group("destmod")),
parseFabricModId(result.getMatcher().group("destmod")));
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.matcher().group("sourcemod")),
parseFabricModId(result.matcher().group("destmod")),
parseFabricModId(result.matcher().group("destmod")));
break;
case MOD_RESOLUTION_MISSING_MINECRAFT:
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("mod")),
result.getMatcher().group("version"));
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.matcher().group("mod")),
result.matcher().group("version"));
break;
case MOD_FOREST_OPTIFINE:
case TWILIGHT_FOREST_OPTIFINE:
@@ -207,15 +207,15 @@ public class GameCrashWindow extends Stage {
case JADE_FOREST_OPTIFINE:
case NEOFORGE_FOREST_OPTIFINE:
message = i18n("game.crash.reason.mod", "OptiFine");
LOG.info("Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
LOG.info("Crash cause: " + result.rule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
break;
default:
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
Arrays.stream(result.rule().getGroupNames()).map(groupName -> result.matcher().group(groupName))
.toArray());
break;
}
LOG.info("Crash cause: " + result.getRule() + ": " + message);
LOG.info("Crash cause: " + result.rule() + ": " + message);
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
segments.add(new Text("\n\n"));
}
@@ -338,6 +338,13 @@ public class InstallerItem extends Control {
throw new AssertionError("Unknown state type: " + state.getClass());
}
}, control.resolvedStateProperty));
FXUtils.onChangeAndOperate(
control.resolvedStateProperty,
state -> statusLabel.pseudoClassStateChanged(
PseudoClass.getPseudoClass("incompatible"),
state instanceof IncompatibleState
)
);
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
@@ -118,7 +118,7 @@ public final class MemoryStatusBar extends Control {
}
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
double total = status.getTotal();
double total = status.total();
double used = status.getUsed();
double allocated = getSkinnable().getMemoryAllocated();
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.canvas.Canvas;
@@ -86,6 +87,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
center.getChildren().setAll(canvas, item);
root.setCenter(center);
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
@@ -119,6 +121,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = FXUtils.newToggleButton4(SVG.REFRESH);
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
spinnerRefresh.setPrefSize(30.0, 30.0);
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
btnRefresh.setDisable(true);
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot.tooltip"));
@@ -157,12 +160,9 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
right.getChildren().add(spinnerUpload);
JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
SpinnerPane spinnerCopyUUID = new SpinnerPane();
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getProfileID().toString()));
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
spinnerCopyUUID.setContent(btnCopyUUID);
right.getChildren().add(spinnerCopyUUID);
right.getChildren().add(btnCopyUUID);
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
@@ -87,8 +87,22 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
actions.getChildren().setAll(cancel, nextPane);
}
addServerPane.setBody(txtServerUrl);
addServerPane.setActions(lblCreationWarning, actions);
VBox body = new VBox();
body.setSpacing(8);
lblCreationWarning.setMinWidth(0);
lblCreationWarning.setPrefWidth(0);
lblCreationWarning.maxWidthProperty().bind(body.widthProperty());
lblCreationWarning.visibleProperty().bind(lblCreationWarning.textProperty().isNotEmpty());
lblCreationWarning.managedProperty().bind(lblCreationWarning.visibleProperty());
body.getChildren().addAll(
txtServerUrl,
lblCreationWarning
);
addServerPane.setBody(body);
addServerPane.setActions(actions);
txtServerUrl.getValidators().addAll(new RequiredValidator(), new URLValidator());
FXUtils.setValidateWhileTextChanged(txtServerUrl, true);
@@ -154,7 +168,6 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
this.setContent(addServerPane, ContainerAnimations.NONE);
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
nextPane.hideSpinner();
onEscPressed(this, this::onAddCancel);
@@ -148,14 +148,14 @@ public class OfflineAccountSkinPane extends StackPane {
UUID uuid = this.account.getProfileID();
if (result == null || result.skin() == null && result.cape() == null) {
canvas.updateSkin(
TexturesLoader.getDefaultSkin(uuid).getImage(),
TexturesLoader.getDefaultSkin(uuid).image(),
TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM,
null
);
return;
}
canvas.updateSkin(
result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).getImage(),
result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).image(),
result.model() == TextureModel.SLIM,
result.cape() != null ? result.cape().image() : null);
}
@@ -29,7 +29,10 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
public class ComponentList extends Control implements NoPaddingComponent {
@@ -136,13 +139,25 @@ public class ComponentList extends Control implements NoPaddingComponent {
}
public static Node createComponentListTitle(String title) {
HBox node = new HBox();
return createComponentListTitle(title, null);
}
public static Node createComponentListTitle(String title, @Nullable String helpMessage) {
HBox node = new HBox(4);
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(8, 0, 0, 0));
{
Label advanced = new Label(title);
node.getChildren().setAll(advanced);
}
if (helpMessage != null) {
var helpIcon = new StackPane(SVG.HELP.createIcon(16));
FXUtils.installFastTooltip(helpIcon, helpMessage);
node.getChildren().add(helpIcon);
}
return node;
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.Control;
@@ -228,4 +229,30 @@ public class ComponentSublist extends Control implements NoPaddingComponent {
public void setTip(String tip) {
this.tip.set(tip);
}
/// Whether the sublist header should use a larger title font.
private static final PseudoClass PSEUDO_LARGE_TITLE = PseudoClass.getPseudoClass("large-title");
/// The property controlling whether the sublist header uses a larger title font.
private final BooleanProperty largeTitle = new SimpleBooleanProperty(this, "largeTitle", false) {
@Override
protected void invalidated() {
pseudoClassStateChanged(PSEUDO_LARGE_TITLE, get());
}
};
/// Returns the property controlling whether the sublist header uses a larger title font.
public BooleanProperty largeTitleProperty() {
return largeTitle;
}
/// Returns whether the sublist header uses a larger title font.
public final boolean isLargeTitle() {
return largeTitle.get();
}
/// Sets whether the sublist header uses a larger title font.
public final void setLargeTitle(boolean value) {
largeTitle.set(value);
}
}
@@ -145,10 +145,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
});
});
Node headerLeft = sublist.getHeaderLeft();
if (headerLeft != null) {
header.setTitleContent(headerLeft);
}
FXUtils.onChangeAndOperate(sublist.largeTitleProperty(), header::setLargeTitle);
InvalidationListener updateTitleTrailing = observable -> {
Node titleTrailing = sublist.getTitleTrailing();
@@ -23,8 +23,8 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
@@ -43,11 +43,10 @@ import org.jackhuang.hmcl.util.MathUtils;
public class FloatScrollBarSkin implements Skin<ScrollBar> {
private ScrollBar scrollBar;
private Region group;
private Rectangle track = new Rectangle();
private Rectangle thumb = new Rectangle();
private final Rectangle track = new Rectangle();
private final Rectangle thumb = new Rectangle();
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
private ChangeListener<Boolean> thumbHoverListener;
private BooleanBinding hoverOrPressed;
private Animation thumbHoverAnimation;
public FloatScrollBarSkin(final ScrollBar scrollBar) {
@@ -214,7 +213,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
}
};
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
this.hoverOrPressed = thumb.hoverProperty().or(thumb.pressedProperty());
FXUtils.onChangeAndOperate(hoverOrPressed, newValue -> {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
@@ -249,7 +249,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
thumbHoverListener = null;
hoverOrPressed = null;
scrollBar = null;
group = null;
}
@@ -26,10 +26,12 @@ import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jetbrains.annotations.Nullable;
@@ -274,6 +276,8 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
/// Updates the fixed preferred height after the row width or subtitle content changes.
private void updatePreferredHeight() {
if (FXUtils.getLimitHeight(this) > 0) return;
double width = getWidth();
if (width <= 0) {
setMinHeight(MIN_HEIGHT);
@@ -53,8 +53,13 @@ public abstract class MDListCell<T> extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
if (oldItem == item && oldEmpty == empty) return;
updateControl(item, empty);
if (empty) {
setGraphic(null);
@@ -85,8 +85,8 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
group.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
Choice<T> choice = newValue != null ? choiceByToggle.get(newValue) : null;
selectedChoice.set(choice);
selectedValue.set(choice != null ? choice.getValue() : null);
selectedChoice.set(choice);
});
selectedValue.addListener((observable, oldValue, newValue) -> {
@@ -96,6 +96,12 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
});
}
/// Replaces the displayed choices.
@SafeVarargs
public final void setChoices(Choice<T>... choices) {
setChoices(List.of(choices));
}
/// Replaces the displayed choices.
public void setChoices(Collection<? extends Choice<T>> choices) {
this.choices.setAll(choices);
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.theme.LauncherBackground;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
@@ -49,7 +50,9 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class Decorator extends Control {
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ObjectProperty<Background> contentBackground = new SimpleObjectProperty<>();
/// The launcher background and opacity rendered by this decorator's skin.
private final ObjectProperty<LauncherBackground> contentBackground = new SimpleObjectProperty<>();
private final ObjectProperty<DecoratorPage.State> state = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onCloseNavButtonAction = new SimpleObjectProperty<>();
private final ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonAction = new SimpleObjectProperty<>();
@@ -151,15 +154,18 @@ public class Decorator extends Control {
this.container.set(container);
}
public Background getContentBackground() {
/// Returns the launcher background rendered behind the content.
public LauncherBackground getContentBackground() {
return contentBackground.get();
}
public ObjectProperty<Background> contentBackgroundProperty() {
/// Returns the launcher background property rendered by this decorator's skin.
public ObjectProperty<LauncherBackground> contentBackgroundProperty() {
return contentBackground;
}
public void setContentBackground(Background contentBackground) {
/// Sets the launcher background rendered behind the content.
public void setContentBackground(LauncherBackground contentBackground) {
this.contentBackground.set(contentBackground);
}
@@ -44,27 +44,11 @@ public interface DecoratorPage extends Refreshable {
((Node) this).getStyleClass().add("content-background");
}
class State {
private final String title;
private final Node titleNode;
private final boolean backable;
private final boolean refreshable;
private final boolean animate;
private final double leftPaneWidth;
record State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate) {
this(title, titleNode, backable, refreshable, animate, 0);
}
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
this.title = title;
this.titleNode = titleNode;
this.backable = backable;
this.refreshable = refreshable;
this.animate = animate;
this.leftPaneWidth = leftPaneWidth;
}
public static State fromTitle(String title) {
return new State(title, null, true, false, true);
}
@@ -76,29 +60,5 @@ public interface DecoratorPage extends Refreshable {
public static State fromTitleNode(Node titleNode) {
return new State(null, titleNode, true, false, true);
}
public String getTitle() {
return title;
}
public Node getTitleNode() {
return titleNode;
}
public boolean isBackable() {
return backable;
}
public boolean isRefreshable() {
return refreshable;
}
public boolean isAnimate() {
return animate;
}
public double getLeftPaneWidth() {
return leftPaneWidth;
}
}
}
@@ -44,6 +44,7 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.glavo.monetfx.ColorRole;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -134,9 +135,33 @@ public class DecoratorSkin extends SkinBase<Decorator> {
root.getChildren().setAll(shadowContainer);
StackPane wrapper = new StackPane();
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
() -> Themes.windowTransparentProperty().get()
? null
: new Background(new BackgroundFill(
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
CornerRadii.EMPTY,
Insets.EMPTY)),
Themes.windowTransparentProperty(),
Themes.colorSchemeProperty()));
Region backgroundNode = new Region();
backgroundNode.setMouseTransparent(true);
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
() -> skinnable.getContentBackground() == null
? null
: skinnable.getContentBackground().background(),
skinnable.contentBackgroundProperty()));
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
() -> skinnable.getContentBackground() == null
? 1.0
: skinnable.getContentBackground().opacity(),
skinnable.contentBackgroundProperty()));
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
BorderPane frame = new BorderPane();
frame.getStyleClass().addAll("jfx-decorator");
wrapper.getChildren().setAll(frame);
wrapper.getChildren().setAll(backgroundNode, frame);
skinnable.setDrawerWrapper(wrapper);
parent.getChildren().add(wrapper);
@@ -178,20 +203,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
titleContainer = new StackPane();
titleContainer.setPickOnBounds(false);
titleContainer.getStyleClass().addAll("jfx-tool-bar");
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
() -> Math.max(0.0, wrapper.getHeight()
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
wrapper.heightProperty(),
skinnable.titleTransparentProperty(),
titleContainer.heightProperty()));
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
// and decide whether the whole top bar should be rendered in white or black. TODO
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
if (titleTransparent) {
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
container.backgroundProperty().unbind();
container.setBackground(null);
titleContainer.getStyleClass().remove("background");
titleContainer.getStyleClass().add("gray-background");
} else {
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
wrapper.backgroundProperty().unbind();
wrapper.setBackground(null);
titleContainer.getStyleClass().add("background");
titleContainer.getStyleClass().remove("gray-background");
}
@@ -208,8 +233,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
navBarPane.setId("decoratorTitleTransitionPane");
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
if (s == null) return;
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
if (s.isAnimate()) {
Node node = createNavBar(skinnable, s.leftPaneWidth(), s.backable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.refreshable(), s.title(), s.titleNode());
if (s.animate()) {
TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
case NEXT -> NavBarAnimations.NEXT;
case PREVIOUS -> NavBarAnimations.PREVIOUS;
@@ -57,7 +57,7 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
if (to instanceof DecoratorPage) {
state.bind(Bindings.createObjectBinding(() -> {
State state = ((DecoratorPage) to).stateProperty().get();
return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true, leftPaneWidth.get());
return new State(state.title(), state.titleNode(), backable.get(), state.refreshable(), true, leftPaneWidth.get());
}, ((DecoratorPage) to).stateProperty()));
} else {
state.unbind();
@@ -318,9 +318,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -61,11 +61,12 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
{
var archiveNamePane = new LinePane();
{
FXUtils.setLimitHeight(archiveNamePane, 75);
archiveNamePane.setTitle(i18n("version.name"));
txtModpackName = new JFXTextField();
txtModpackName.setPrefWidth(300);
FXUtils.setLimitHeight(archiveNamePane, 75);
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setRight(txtModpackName);
@@ -61,9 +61,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
})
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -213,6 +213,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
RemoteVersion oldRemoteVersion = getItem();
super.updateItem(remoteVersion, empty);
if (empty) {
@@ -221,6 +222,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
}
setGraphic(pane);
if (oldRemoteVersion == remoteVersion) return;
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
if (remoteVersion.getReleaseDate() != null) {
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
@@ -228,67 +231,65 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
twoLineListItem.setSubtitle(null);
}
if (oldRemoteVersion != remoteVersion) {
twoLineListItem.getTags().clear();
twoLineListItem.getTags().clear();
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
switch (versionType) {
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
}
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
switch (versionType) {
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.OPTIFINE;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof CleanroomRemoteVersion)
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(displayGameVersion);
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.OPTIFINE;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof CleanroomRemoteVersion)
iconType = VersionIconType.CLEANROOM;
else if (remoteVersion instanceof NeoForgeRemoteVersion)
iconType = VersionIconType.NEO_FORGE;
else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion)
iconType = VersionIconType.LEGACY_FABRIC;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = VersionIconType.COMMAND;
imageView.setImage(iconType.getIcon());
String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()));
if (twoLineListItem.getSubtitle() == null)
twoLineListItem.setSubtitle(displayGameVersion);
else
twoLineListItem.addTag(displayGameVersion);
}
}
}
@@ -390,7 +391,12 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
list = new JFXListView<>();
list.getStyleClass().add("jfx-list-view-float");
list.setFixedCellSize(65);
RemoteVersionListCell dummyCell = new RemoteVersionListCell(control);
dummyCell.twoLineListItem.setTitle("Dummy");
dummyCell.twoLineListItem.setSubtitle("Dummy");
dummyCell.imageView.setImage(VersionIconType.GRASS.getIcon());
prepareNode(dummyCell.pane);
list.setFixedCellSize(dummyCell.pane.prefHeight(-1));
VBox.setVgrow(list, Priority.ALWAYS);
@@ -190,7 +190,7 @@ public final class ExportWizardProvider implements WizardProvider {
"OneSix",
exportInfo.getName() + "-" + exportInfo.getVersion(),
null,
Lang.toIntOrNull(setting.get(GameSettings::permSizeProperty)),
Lang.toIntOrNull(setting.getInheritable(GameSettings::permSizeProperty)),
setting.getInheritable(GameSettings::commandWrapperProperty),
setting.getInheritable(GameSettings::preLaunchCommandProperty),
null,
@@ -101,9 +101,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse(""));
GameSettings.Effective versionSetting = gameRepository.getEffectiveGameSettings(versionName);
minMemory.set(Optional.ofNullable(versionSetting.get(GameSettings::minMemoryProperty)).orElse(0));
launchArguments.set(versionSetting.get(GameSettings::gameArgumentsProperty));
javaArguments.set(versionSetting.get(GameSettings::jvmOptionsProperty));
minMemory.set(Optional.ofNullable(versionSetting.getInheritable(GameSettings::minMemoryProperty)).orElse(0));
launchArguments.set(versionSetting.getInheritable(GameSettings::gameArgumentsProperty));
javaArguments.set(versionSetting.getInheritable(GameSettings::jvmOptionsProperty));
canIncludeLauncher = JarUtils.thisJarPath() != null;
}
@@ -22,7 +22,10 @@ import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -32,10 +35,7 @@ import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
@@ -70,7 +70,6 @@ import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -101,9 +100,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// The current setting.
private final ObjectProperty<@Nullable S> currentSetting = new SimpleObjectProperty<>(this, "setting");
private final ObjectProperty<GameVersionNumber> currentGameVersionNumber = new SimpleObjectProperty<>(this, "currentGameVersionNumber", GameVersionNumber.unknown());
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
private boolean updatingParentSetting = false;
private final ObjectProperty<GameSettings.@Nullable Preset> activeParentSetting =
new SimpleObjectProperty<>(this, "activeParentSetting");
// GUI
private final ScrollPane scrollPane;
@@ -130,6 +133,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class;
this.isPresetSetting = settingType == GameSettings.Preset.class;
if (!isPresetSetting) {
bindActiveParentSetting();
}
this.scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -155,11 +161,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
{
if (isPresetSetting) {
var presetSettings = new ComponentList();
rootPane.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.type.global.preset")),
presetSettings,
ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
basicSettings,
ComponentList.createComponentListTitle(
i18n("settings.type.global.preset"),
i18n("settings.type.global.preset.help")
),
presetSettings,
ComponentList.createComponentListTitle(i18n("settings.game.section.game")),
gameSettings,
ComponentList.createComponentListTitle(i18n("settings.launcher")),
@@ -188,9 +196,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
var parentGameSettingsPane = new LineSelectButton<GameSettings.@Nullable Preset>();
basicSettings.getContent().add(parentGameSettingsPane);
parentGameSettingsPane.setTitle(i18n("settings.type.global.preset"));
parentGameSettingsPane.setSubtitle(i18n("settings.type.global.preset.subtitle"));
parentGameSettingsPane.setConverter(setting -> setting != null
? PresetManagementPane.getPresetDisplayName(setting)
: getImplicitParentGameSettingsDisplayName());
: i18n("settings.type.global.preset.default"));
bindInstanceParentSetting(parentGameSettingsPane);
}
@@ -271,9 +280,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refreshJavaSettings();
});
SettingsManager.getGameSettings().addListener(weakJavaListener);
settings().defaultGameSettingsPresetProperty().addListener(weakJavaListener);
activeParentSetting.addListener(weakJavaListener);
javaItem.selectedChoiceProperty().addListener((observable, oldChoice, newChoice) -> {
S setting = currentSetting.get();
if (setting == null || updatingSelectedJava) {
@@ -339,15 +346,15 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
defaultIsolationTypePane.setSubtitle(i18n("settings.game.default_isolation.subtitle"));
defaultIsolationTypePane.setItems(DefaultIsolationType.values());
defaultIsolationTypePane.setNullSafeConverter(type -> switch (type) {
case NEVER -> i18n("settings.game.default_isolation.never");
case ALWAYS -> i18n("settings.game.default_isolation.always");
case MODDED -> i18n("settings.game.default_isolation.modded");
});
case NEVER -> i18n("settings.game.default_isolation.never");
case ALWAYS -> i18n("settings.game.default_isolation.always");
case MODDED -> i18n("settings.game.default_isolation.modded");
});
defaultIsolationTypePane.setDescriptionConverter(type -> switch (type) {
case NEVER -> i18n("settings.game.default_isolation.never.desc");
case ALWAYS -> i18n("settings.game.default_isolation.always.desc");
case MODDED -> i18n("settings.game.default_isolation.modded.desc");
});
case NEVER -> i18n("settings.game.default_isolation.never.desc");
case ALWAYS -> i18n("settings.game.default_isolation.always.desc");
case MODDED -> i18n("settings.game.default_isolation.modded.desc");
});
bindPresetBidirectional(defaultIsolationTypePane.valueProperty(), GameSettings.Preset::defaultIsolationTypeProperty);
} else {
var isolationButton = new LineToggleButton();
@@ -359,6 +366,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
// Memory Setting
@Nullable JFXButton autoMemoryButton = !isPresetSetting ? createInheritanceButton() : null;
ObjectProperty<Boolean> memorySelectedValue = new SimpleObjectProperty<>(true);
var memorySublist = new ComponentSublist(() -> {
var memoryItem = new RadioChoiceList<Boolean>();
memoryItem.setFallbackValue(true);
@@ -378,16 +386,18 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
maxMemoryButton = createInheritanceButton();
}
var options = new ArrayList<RadioChoiceList.Choice<Boolean>>();
options.add(new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true));
var manualMemoryEditor = new HBox(8);
manualMemoryEditor.setAlignment(Pos.CENTER_RIGHT);
manualMemoryEditor.getChildren().setAll(
maxMemorySlider,
maxMemoryTextField,
new Label(i18n("settings.memory.unit.mib")));
options.add(new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton));
memoryItem.setChoices(options);
memoryItem.setChoices(List.of(
new RadioChoiceList.Choice<>(i18n("settings.memory.auto_allocate"), true),
new EditorChoice<>(i18n("settings.memory.manual_allocate"), false, manualMemoryEditor, maxMemoryButton)
));
memoryItem.selectedValueProperty().addListener((observable, oldValue, newValue) ->
memorySelectedValue.set(newValue));
var memoryStatusBar = new MemoryStatusBar();
@@ -415,7 +425,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
maxMemoryButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
return List.of(memoryItem, memoryStatusPane);
});
@@ -425,7 +436,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
gameSettings.getContent().add(memorySublist);
memorySublist.setTitle(i18n("settings.memory"));
memorySublist.setHasSubtitle(true);
memorySublist.setDescription(i18n("settings.memory.auto_allocate"));
memorySelectedValue.addListener((observable, oldValue, newValue) ->
memorySublist.setDescription(getMemoryAllocationDisplayName(newValue)));
memorySublist.setDescription(getMemoryAllocationDisplayName(memorySelectedValue.get()));
bindMemorySelectedValue(memorySelectedValue);
// Launcher Visibility Setting
var launcherVisibilityPane = createInheritableButton(
@@ -653,6 +667,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
{
var txtMinMemory = new JFXTextField();
txtMinMemory.setPrefWidth(160);
txtMinMemory.setTextFormatter(new TextFormatter<>(change -> {
change.setText(change.getText().replaceAll("[^0-9]", ""));
return change;
}));
var rightBox = new HBox(8, txtMinMemory, new Label(i18n("settings.memory.unit.mib")));
rightBox.setAlignment(Pos.CENTER_RIGHT);
@@ -666,6 +684,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
{
var txtMetaspace = new JFXTextField();
txtMetaspace.setPrefWidth(160);
txtMetaspace.setTextFormatter(new TextFormatter<>(change -> {
change.setText(change.getText().replaceAll("[^0-9]", ""));
return change;
}));
var rightBox = new HBox(8, txtMetaspace, new Label(i18n("settings.memory.unit.mib")));
rightBox.setAlignment(Pos.CENTER_RIGHT);
@@ -740,6 +762,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
graphicsSettings.getContent().add(graphicsBackendPane);
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
if (isPresetSetting)
graphicsBackendPane.setSubtitle(i18n("settings.advanced.graphics_backend.desc"));
var openGLRendererPane = createInheritableButton(
GameSettings::openGLRendererProperty,
e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)),
@@ -762,6 +787,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
graphicsSettings.getContent().add(vulkanRendererPane);
vulkanRendererPane.setTitle(i18n("settings.advanced.renderer.vulkan"));
this.currentGameVersionNumber.addListener((o, oldValue, newValue) -> {
boolean showBackendChoose = isPresetSetting || newValue.compareTo("26.2-snapshot-2") >= 0;
graphicsBackendPane.setVisible(showBackendChoose);
graphicsBackendPane.setManaged(showBackendChoose);
boolean showOpenGL = GraphicsAPI.OPENGL.isSupported(newValue);
openGLRendererPane.setVisible(showOpenGL);
openGLRendererPane.setManaged(showOpenGL);
boolean showVulkan = GraphicsAPI.VULKAN.isSupported(newValue);
vulkanRendererPane.setVisible(showVulkan);
vulkanRendererPane.setManaged(showVulkan);
});
}
var nativeLibrarySettings = new ComponentList();
@@ -810,18 +848,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private void bindInstanceParentSetting(LineSelectButton<GameSettings.@Nullable Preset> button) {
ObservableList<GameSettings.Preset> items = FXCollections.observableArrayList();
ObservableList<GameSettings.Preset> presets = getAvailableParentPresets();
InvalidationListener updateItems = observable -> {
@Nullable GameSettings.Preset selected = button.getValue();
items.setAll((GameSettings.Preset) null);
items.addAll(SettingsManager.getGameSettings());
items.addAll(presets);
refreshInstanceParentSettingConverter(button);
if (selected != null && SettingsManager.getGameSettings(selected.idProperty().getValue()) == null) {
if (selected != null && getAvailableParentPreset(selected.idProperty().getValue()) == null) {
button.setValue(null);
}
};
InvalidationListener weakUpdateItems = holder.weak(updateItems);
updateItems.invalidated(SettingsManager.getGameSettings());
SettingsManager.getGameSettings().addListener(weakUpdateItems);
updateItems.invalidated(presets);
presets.addListener(weakUpdateItems);
button.setItems(items);
refreshInstanceParentSettingConverter(button);
@@ -838,7 +877,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
try {
refreshInstanceParentSettingConverter(button);
GameSettingsPresetID parent = setting.parentProperty().getValue();
button.setValue(parent != null ? SettingsManager.getGameSettings(parent) : null);
button.setValue(parent != null ? getAvailableParentPreset(parent) : null);
} finally {
updatingParentSetting = false;
}
@@ -846,28 +885,54 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
});
}
/// Refreshes parent preset display text because the implicit parent depends on the current game directory.
/// Refreshes parent preset display text after the preset list changes.
private void refreshInstanceParentSettingConverter(LineSelectButton<GameSettings.@Nullable Preset> button) {
button.setConverter(setting -> setting != null
? PresetManagementPane.getPresetDisplayName(setting)
: getImplicitParentGameSettingsDisplayName());
: i18n("settings.type.global.preset.default"));
}
/// Returns the label for the implicit parent preset selected by a null instance parent.
private String getImplicitParentGameSettingsDisplayName() {
GameSettings.Preset legacyParent = getGameDirectoryLegacyGameSettings();
return legacyParent != null
? PresetManagementPane.getPresetDisplayName(legacyParent)
: i18n("settings.type.global.preset.default");
/// Returns the presets that can be selected as an instance parent.
private ObservableList<GameSettings.Preset> getAvailableParentPresets() {
return SettingsManager.gameSettingsPresets().getPresets();
}
/// Returns the migrated game-directory-level parent preset, or `null` when this game directory uses the default preset.
private @Nullable GameSettings.Preset getGameDirectoryLegacyGameSettings() {
if (gameDirectory == null || gameDirectory.getLegacyGameSettings() == null) {
return null;
/// Returns the preset selected by an instance parent ID.
private GameSettings.@Nullable Preset getAvailableParentPreset(@Nullable GameSettingsPresetID id) {
return SettingsManager.gameSettingsPresets().getPreset(id);
}
/// Keeps [#activeParentSetting] synchronized with the current instance's resolved parent preset.
private void bindActiveParentSetting() {
InvalidationListener refresh = observable -> refreshActiveParentSetting();
InvalidationListener weakRefresh = holder.weak(refresh);
getAvailableParentPresets().addListener(weakRefresh);
SettingsManager.settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeListener(weakRefresh);
}
if (newValue != null) {
newValue.addListener(weakRefresh);
}
refreshActiveParentSetting();
});
S setting = currentSetting.get();
if (setting != null) {
setting.addListener(weakRefresh);
}
refreshActiveParentSetting();
}
return SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
/// Refreshes the current instance's resolved parent preset.
private void refreshActiveParentSetting() {
activeParentSetting.set(currentSetting.get() instanceof GameSettings.Instance instance
? getEffectiveParentGameSettings(instance)
: null);
}
/// Adds the title-line inheritance button for the Java selection sublist.
@@ -1023,7 +1088,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
this::createInheritanceButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
}
/// Binds an integer text field to a setting property with independent override state.
@@ -1040,7 +1106,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
this::createInheritanceButton,
GameSettingsPage::updateInheritanceButton,
holder,
this::getEffectiveParentGameSettings);
this::getEffectiveParentGameSettings,
activeParentSetting);
}
/// Binds the windowed resolution selector to the width and height settings.
@@ -1083,6 +1150,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
ChangeListener<@Nullable Boolean> focusedListener = (observable, oldValue, newValue) -> {
if (!newValue) {
@@ -1168,8 +1236,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
widthProperty.addListener(weakPropertyListener);
heightProperty.addListener(weakPropertyListener);
}
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
propertyListener.invalidated(setting);
}
@@ -1406,7 +1472,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1424,6 +1494,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
textProperty.addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1489,9 +1560,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<String> property = propertyGetter.apply(setting);
@@ -1514,7 +1582,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1532,6 +1604,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
textProperty.addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1597,9 +1670,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<String> property = propertyGetter.apply(setting);
@@ -1650,6 +1720,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
});
currentSetting.addListener((observable, oldValue, newValue) -> {
@@ -1718,6 +1790,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
GameSettings setting = currentSetting.get();
@@ -1737,6 +1810,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
event.consume();
});
@@ -1756,6 +1832,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
} finally {
updating.value = false;
}
fireEvent(new VersionPage.WorkingDirChangedEvent());
});
currentSetting.addListener((observable, oldValue, newValue) -> {
@@ -1769,9 +1847,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting instanceof GameSettings.Instance instance) {
instance.addListener(weakRefresh);
@@ -1815,13 +1890,96 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
}
/// Keeps a listener attached to the current instance's parent preset setting property.
private <T extends @UnknownNullability Object> void updateParentSettingPropertyListener(
@Nullable GameSettings setting,
ObjectProperty<@Nullable SettingProperty<T>> activeParentProperty,
Function<GameSettings, ? extends SettingProperty<T>> propertyGetter,
InvalidationListener listener) {
SettingProperty<T> oldParentProperty = activeParentProperty.get();
SettingProperty<T> newParentProperty = setting instanceof GameSettings.Instance instance
? propertyGetter.apply(getEffectiveParentGameSettings(instance))
: null;
if (oldParentProperty == newParentProperty) {
return;
}
if (oldParentProperty != null) {
oldParentProperty.removeListener(listener);
}
activeParentProperty.set(newParentProperty);
if (newParentProperty != null) {
newParentProperty.addListener(listener);
}
}
/// Binds the memory sublist description source to the effective automatic memory setting.
private void bindMemorySelectedValue(ObjectProperty<Boolean> memorySelectedValue) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
S setting = currentSetting.get();
updateParentSettingPropertyListener(
setting,
activeParentAutoMemoryProperty,
GameSettings::autoMemoryProperty,
refreshHolder.value);
if (setting == null) {
memorySelectedValue.set(true);
return;
}
memorySelectedValue.set(resolveEffectiveSetting(setting)
.getInheritable(GameSettings::autoMemoryProperty));
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeListener(weakRefresh);
}
SettingProperty<Boolean> oldAutoMemoryProperty = activeAutoMemoryProperty.get();
if (oldAutoMemoryProperty != null) {
oldAutoMemoryProperty.removeListener(weakRefresh);
}
SettingProperty<Boolean> newAutoMemoryProperty = newValue != null
? newValue.autoMemoryProperty()
: null;
activeAutoMemoryProperty.set(newAutoMemoryProperty);
if (newValue != null) {
newValue.addListener(weakRefresh);
}
if (newAutoMemoryProperty != null) {
newAutoMemoryProperty.addListener(weakRefresh);
}
refresh.invalidated(newValue);
});
S setting = currentSetting.get();
if (setting != null) {
SettingProperty<Boolean> autoMemoryProperty = setting.autoMemoryProperty();
activeAutoMemoryProperty.set(autoMemoryProperty);
setting.addListener(weakRefresh);
autoMemoryProperty.addListener(weakRefresh);
}
refresh.invalidated(setting);
}
/// Binds a radio choice list to an inheritable setting property.
private <T> void bindInheritableRadioChoiceList(
ComponentSublist sublist,
RadioChoiceList<T> item,
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -1831,6 +1989,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener propertyListener = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(
setting,
activeParentProperty,
propertyGetter,
refreshHolder.value);
InheritableProperty<T> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1847,6 +2010,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
ChangeListener<@Nullable T> itemListener = (observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -1911,9 +2076,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
propertyListener.invalidated(newProperty);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
InheritableProperty<T> property = propertyGetter.apply(setting);
@@ -1927,7 +2089,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Binds the window type editor to the window type setting.
private void bindWindowSettings(ComponentSublist sublist, RadioChoiceList<GameWindowType> item) {
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeWindowTypeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<GameWindowType>> activeParentWindowTypeProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -1937,6 +2101,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener propertyListener = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(
setting,
activeParentWindowTypeProperty,
GameSettings::windowTypeProperty,
refreshHolder.value);
InheritableProperty<GameWindowType> property = activeWindowTypeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -1953,6 +2122,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
item.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -2012,9 +2183,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
propertyListener.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
activeWindowTypeProperty.set(setting.windowTypeProperty());
@@ -2047,8 +2215,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private <T> void bindInheritableSublistDescription(ComponentSublist sublist,
Function<GameSettings, InheritableProperty<T>> propertyGetter,
Function<T, String> converter) {
InvalidationListener propertyListener = observable -> initInheritableSublistDescription(sublist, propertyGetter, converter);
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener propertyListener = observable -> {
updateParentInheritablePropertyListener(
currentSetting.get(),
activeParentProperty,
propertyGetter,
refreshHolder.value);
initInheritableSublistDescription(sublist, propertyGetter, converter);
};
InvalidationListener weakPropertyListener = holder.weak(propertyListener);
refreshHolder.value = weakPropertyListener;
activeParentSetting.addListener(weakPropertyListener);
currentSetting.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
@@ -2059,16 +2238,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
propertyGetter.apply(newValue).addListener(weakPropertyListener);
}
initInheritableSublistDescription(sublist, propertyGetter, converter);
propertyListener.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakPropertyListener);
settings().defaultGameSettingsPresetProperty().addListener(weakPropertyListener);
S setting = currentSetting.get();
if (setting != null) {
propertyGetter.apply(setting).addListener(weakPropertyListener);
}
initInheritableSublistDescription(sublist, propertyGetter, converter);
propertyListener.invalidated(setting);
}
private <T> void initInheritableSublistDescription(ComponentSublist sublist,
@@ -2083,6 +2259,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
sublist.setDescription(converter.apply(getEffectiveValue(setting, propertyGetter)));
}
/// Returns the localized memory allocation mode display name.
private static String getMemoryAllocationDisplayName(Boolean autoMemory) {
return i18n(Boolean.TRUE.equals(autoMemory)
? "settings.memory.auto_allocate"
: "settings.memory.manual_allocate");
}
private static String getWindowTypeDisplayName(GameWindowType type) {
return switch (type) {
case FULLSCREEN -> i18n("settings.game.window_type.fullscreen");
@@ -2129,7 +2312,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
button.setOverriddenTooltip(i18n("settings.game.override_global"));
button.setInheritAvailable(!isPresetSetting);
IndependentSettingBinder.bindToggleButton(currentSetting, button, propertyGetter, holder, this::getEffectiveParentGameSettings);
IndependentSettingBinder.bindToggleButton(
currentSetting,
button,
propertyGetter,
holder,
this::getEffectiveParentGameSettings,
activeParentSetting);
return button;
}
@@ -2150,7 +2339,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
LineSelectButton<T> button,
Function<GameSettings, InheritableProperty<T>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<T>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<T>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton = null;
if (!isPresetSetting) {
inheritButton = createInheritanceButton();
@@ -2160,6 +2351,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
InheritableProperty<T> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -2176,6 +2368,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
button.valueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -2241,9 +2435,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
activeProperty.set(propertyGetter.apply(setting));
@@ -2258,10 +2449,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
LineInheritableToggleButton button,
Function<GameSettings, InheritableProperty<Boolean>> propertyGetter) {
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable InheritableProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentInheritablePropertyListener(setting, activeParentProperty, propertyGetter, refreshHolder.value);
InheritableProperty<Boolean> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -2270,7 +2464,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = true;
try {
boolean overridden = isPropertyOverridden(setting, property);
button.setRawValue(overridden ? getDirectValue(property) : getEffectiveValue(setting, propertyGetter));
button.setRawValue(overridden
? getDirectValue(property)
: getEffectiveValue(setting, propertyGetter));
button.setOverridden(overridden);
button.setEffectiveValue(getEffectiveValue(setting, propertyGetter));
} finally {
@@ -2278,6 +2474,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
};
InvalidationListener weakRefresh = holder.weak(refresh);
refreshHolder.value = weakRefresh;
activeParentSetting.addListener(weakRefresh);
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
InheritableProperty<Boolean> property = activeProperty.get();
@@ -2328,9 +2526,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
S setting = currentSetting.get();
if (setting != null) {
activeProperty.set(propertyGetter.apply(setting));
@@ -2399,7 +2594,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Returns the explicitly configured parent preset for an instance, falling back to the default preset.
private GameSettings.Preset getExplicitParentGameSettings(GameSettings.Instance instance) {
GameSettingsPresetID parent = instance.parentProperty().getValue();
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
GameSettings.Preset parentSetting = getAvailableParentPreset(parent);
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
@@ -2438,6 +2633,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
assert isPresetSetting == (instanceId == null);
if (instanceId != null) {
this.currentGameVersionNumber.set(GameVersionNumber.asGameVersion(repository.getGameVersion(instanceId)));
@Nullable GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
this.currentSetting.set((S) setting);
setSettingsReadOnly(
@@ -2446,6 +2643,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
setting != null ? this::forceOverwriteInstanceGameSettings : null);
loadIcon();
} else {
this.currentGameVersionNumber.set(GameVersionNumber.unknown());
this.currentSetting.set((S) SettingsManager.getDefaultGameSettingsPresetOrCreate());
setSettingsReadOnly(
SettingsManager.isGameSettingsReadOnly(),
@@ -2457,15 +2655,15 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Updates the page read-only state used when settings cannot be saved safely.
///
/// @param readOnly whether the current settings should be displayed read-only
/// @param message the warning message shown when the page is read-only
/// @param message the warning message shown when the page is read-only
private void setSettingsReadOnly(boolean readOnly, String message) {
setSettingsReadOnly(readOnly, message, null);
}
/// Updates the page read-only state used when settings cannot be saved safely.
///
/// @param readOnly whether the current settings should be displayed read-only
/// @param message the warning message shown when the page is read-only
/// @param readOnly whether the current settings should be displayed read-only
/// @param message the warning message shown when the page is read-only
/// @param forceOverwrite the action used to back up and overwrite the read-only file
private void setSettingsReadOnly(boolean readOnly, String message, @Nullable Runnable forceOverwrite) {
if (readOnly && forceOverwrite != null) {
@@ -2600,13 +2798,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
if (JavaManager.isInitialized()) {
GameVersionNumber gameVersionNumber;
GameVersionNumber gameVersionNumber = this.currentGameVersionNumber.get();
Version version;
if (this.instanceId == null) {
gameVersionNumber = GameVersionNumber.unknown();
version = null;
} else {
gameVersionNumber = repository != null ? GameVersionNumber.asGameVersion(repository.getGameVersion(this.instanceId)) : GameVersionNumber.unknown();
version = repository != null ? repository.getResolvedVersion(this.instanceId) : null;
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -28,7 +29,6 @@ import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.property.SettingProperty;
import org.jackhuang.hmcl.ui.MemoryStatusBar;
@@ -48,7 +48,6 @@ import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.DataSizeUnit.GIGABYTES;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -69,7 +68,8 @@ final class IndependentSettingBinder {
Supplier<JFXButton> inheritanceButtonFactory,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<String>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<String>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
@@ -84,7 +84,12 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(setting, activeParentProperty, propertyGetter, parentGetter, refreshHolder.value);
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<String> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -104,6 +109,7 @@ final class IndependentSettingBinder {
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
textField.textProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -161,9 +167,12 @@ final class IndependentSettingBinder {
Supplier<JFXButton> inheritanceButtonFactory,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Integer>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
@Nullable JFXButton inheritButton;
if (presetSetting) {
inheritButton = null;
@@ -174,6 +183,12 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<Integer> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -193,6 +208,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
textField.textProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -257,12 +274,16 @@ final class IndependentSettingBinder {
@Nullable JFXButton maxMemoryButton,
BiConsumer<JFXButton, Boolean> inheritanceButtonUpdater,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeMaxMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentAutoMemoryProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Integer>> activeParentMaxMemoryProperty = new SimpleObjectProperty<>();
Label physicalMemoryLabel = (Label) memoryStatusLabels.getLeft();
Label allocatedMemoryLabel = (Label) memoryStatusLabels.getRight();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
int totalMemoryMiB = Math.max(1, (int) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize()));
maxMemorySlider.setValueFactory(slider -> Bindings.createStringBinding(
@@ -271,6 +292,18 @@ final class IndependentSettingBinder {
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentAutoMemoryProperty,
GameSettings::autoMemoryProperty,
parentGetter,
refreshHolder.value);
updateParentPropertyListener(
setting,
activeParentMaxMemoryProperty,
GameSettings::maxMemoryProperty,
parentGetter,
refreshHolder.value);
SettingProperty<Boolean> autoMemoryProperty = activeAutoMemoryProperty.get();
SettingProperty<Integer> maxMemoryProperty = activeMaxMemoryProperty.get();
if (setting == null || autoMemoryProperty == null || maxMemoryProperty == null || updating.value) {
@@ -299,6 +332,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
choiceList.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -433,8 +468,6 @@ final class IndependentSettingBinder {
}
refresh.invalidated(newValue);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
memoryStatusBar.memoryStatusProperty().addListener(observable -> {
GameSettings setting = currentSetting.get();
if (setting == null) {
@@ -468,12 +501,21 @@ final class IndependentSettingBinder {
LineInheritableToggleButton button,
Function<GameSettings, SettingProperty<Boolean>> propertyGetter,
WeakListenerHolder listenerHolder,
Function<GameSettings.Instance, GameSettings.Preset> parentGetter) {
Function<GameSettings.Instance, GameSettings.Preset> parentGetter,
Observable parentSettingsInvalidation) {
ObjectProperty<@Nullable SettingProperty<Boolean>> activeProperty = new SimpleObjectProperty<>();
ObjectProperty<@Nullable SettingProperty<Boolean>> activeParentProperty = new SimpleObjectProperty<>();
final Holder<Boolean> updating = new Holder<>(false);
final Holder<InvalidationListener> refreshHolder = new Holder<>();
InvalidationListener refresh = observable -> {
GameSettings setting = currentSetting.get();
updateParentPropertyListener(
setting,
activeParentProperty,
propertyGetter,
parentGetter,
refreshHolder.value);
SettingProperty<Boolean> property = activeProperty.get();
if (setting == null || property == null || updating.value) {
return;
@@ -483,7 +525,9 @@ final class IndependentSettingBinder {
try {
boolean overridden = isOverridden(setting, property);
Boolean effectiveValue = getEffectiveValue(setting, propertyGetter, parentGetter);
button.setRawValue(overridden ? getDirectValue(property) : Boolean.TRUE.equals(effectiveValue));
button.setRawValue(overridden
? getDirectValue(property)
: Boolean.TRUE.equals(effectiveValue));
button.setOverridden(overridden);
button.setEffectiveValue(Boolean.TRUE.equals(effectiveValue));
} finally {
@@ -491,6 +535,8 @@ final class IndependentSettingBinder {
}
};
InvalidationListener weakRefresh = listenerHolder.weak(refresh);
refreshHolder.value = weakRefresh;
parentSettingsInvalidation.addListener(weakRefresh);
button.rawValueProperty().addListener((observable, oldValue, newValue) -> {
GameSettings setting = currentSetting.get();
@@ -503,7 +549,8 @@ final class IndependentSettingBinder {
try {
setOverridden(setting, property, true);
property.setValue(newValue);
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
button.setEffectiveValue(Boolean.TRUE.equals(
getEffectiveValue(setting, propertyGetter, parentGetter)));
} finally {
updating.value = false;
}
@@ -522,7 +569,8 @@ final class IndependentSettingBinder {
if (newValue) {
property.setValue(button.getRawValue());
}
button.setEffectiveValue(Boolean.TRUE.equals(getEffectiveValue(setting, propertyGetter, parentGetter)));
button.setEffectiveValue(Boolean.TRUE.equals(
getEffectiveValue(setting, propertyGetter, parentGetter)));
} finally {
updating.value = false;
}
@@ -569,47 +617,26 @@ final class IndependentSettingBinder {
Label allocatedMemoryLabel,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
memoryStatusBar.memoryAllocatedProperty().set(calculateAllocatedMemory(memoryStatusBar, autoMemory, maxMemory));
updateMemoryLabels(memoryStatusBar, physicalMemoryLabel, allocatedMemoryLabel, autoMemory, maxMemory);
}
private static double calculateAllocatedMemory(
MemoryStatusBar memoryStatusBar,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
return HMCLGameRepository.getAllocatedMemory(
maxMemoryBytes,
memoryStatusBar.getMemoryStatus().getAvailable(),
Boolean.TRUE.equals(autoMemory));
}
private static void updateMemoryLabels(
MemoryStatusBar memoryStatusBar,
Label physicalMemoryLabel,
Label allocatedMemoryLabel,
@Nullable Boolean autoMemory,
@Nullable Integer maxMemory) {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
boolean autoMemoryEnabled = Boolean.TRUE.equals(autoMemory);
long memoryAllocated;
if (Boolean.TRUE.equals(autoMemory)) {
memoryAllocated = HMCLGameRepository.getAutoAllocatedMemory(memoryStatus.available());
} else if (maxMemory != null && maxMemory > 0) {
memoryAllocated = maxMemory * 1024L * 1024L;
} else {
memoryAllocated = GameSettings.SUGGESTED_MEMORY * 1024L * 1024L;
}
memoryStatusBar.memoryAllocatedProperty().set(memoryAllocated);
physicalMemoryLabel.setText(i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.getTotal())));
allocatedMemoryLabel.setText(i18n(
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.getAvailable()
? (autoMemoryEnabled
? "settings.memory.allocate.auto.exceeded"
: "settings.memory.allocate.manual.exceeded")
: (autoMemoryEnabled
? "settings.memory.allocate.auto"
: "settings.memory.allocate.manual"),
GIGABYTES.convertFromBytes(maxMemoryBytes),
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(
maxMemoryBytes,
memoryStatus.getAvailable(),
autoMemoryEnabled)),
GIGABYTES.convertFromBytes(memoryStatus.getAvailable())));
GIGABYTES.convertFromBytes(memoryStatus.total())));
allocatedMemoryLabel.setText(
memoryStatus.hasAvailable() && memoryAllocated > memoryStatus.available()
? i18n("settings.memory.allocate.exceeded", GIGABYTES.convertFromBytes(memoryAllocated), GIGABYTES.convertFromBytes(memoryStatus.available()))
: i18n("settings.memory.allocate", GIGABYTES.convertFromBytes(memoryAllocated)));
}
private static <T> void toggleOverride(
@@ -656,9 +683,6 @@ final class IndependentSettingBinder {
}
refresh.invalidated(newProperty);
});
SettingsManager.getGameSettings().addListener(weakRefresh);
settings().defaultGameSettingsPresetProperty().addListener(weakRefresh);
GameSettings setting = currentSetting.get();
if (setting != null) {
SettingProperty<T> property = propertyGetter.apply(setting);
@@ -76,7 +76,6 @@ final class PresetManagementPane extends ComponentSublist {
this.holder = Objects.requireNonNull(holder);
setTitle(i18n("settings.type.global.preset.manage_all"));
setHasSubtitle(true);
LineButton createButton = new LineButton();
createButton.setTitle(i18n("settings.type.global.preset.create"));
@@ -189,7 +188,7 @@ final class PresetManagementPane extends ComponentSublist {
/// Creates a new preset and selects it for editing.
private void createPreset() {
int number = createDefaultPresetNumber();
int number = SettingsManager.gameSettingsPresets().newPresetAutoNameNumber();
PromptDialogPane.Builder.StringQuestion nameQuestion =
new PromptDialogPane.Builder.StringQuestion("", "")
.setPromptText(i18n("settings.type.global.preset.auto_name", number));
@@ -216,26 +215,6 @@ final class PresetManagementPane extends ComponentSublist {
}).addQuestion(nameQuestion));
}
/// Returns the first automatic preset number that is not used by existing presets.
private int createDefaultPresetNumber() {
for (int index = 1; ; index++) {
String name = i18n("settings.type.global.preset.auto_name", index);
boolean used = false;
for (GameSettings.Preset setting : SettingsManager.getGameSettings()) {
Integer autoNameNumber = setting.autoNameNumberProperty().getValue();
if ((autoNameNumber != null && autoNameNumber == index)
|| Objects.equals(name, getPresetDisplayName(setting))) {
used = true;
break;
}
}
if (!used) {
return index;
}
}
}
/// Asks the user for a new preset name.
private void renamePreset(GameSettings.Preset setting) {
@Nullable String currentName = getPresetCustomName(setting);
@@ -40,20 +40,27 @@ 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;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Arrays;
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;
/**
* @author Glavo
*/
/// Provides image-format detection and loaders for image formats not handled
/// directly by JavaFX.
///
/// @author Glavo
@NotNullByDefault
public final class ImageUtils {
// ImageLoaders
@@ -78,38 +85,37 @@ public final class ImageUtils {
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
SVGImage image;
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;
if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
image = supplier.get();
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
image = CompletableFuture.supplyAsync(supplier, Schedulers.javafx()).get();
}
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);
}
return image;
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
@@ -154,13 +160,13 @@ public final class ImageUtils {
if (doScale) {
targetWidth = requestedWidth;
targetHeight = requestedHeight;
pixels = scale(defaultImage.array,
defaultImage.width, defaultImage.height,
pixels = scale(defaultImage.array(),
defaultImage.width(), defaultImage.height(),
targetWidth, targetHeight);
} else {
targetWidth = defaultImage.width;
targetHeight = defaultImage.height;
pixels = defaultImage.array;
targetWidth = defaultImage.width();
targetHeight = defaultImage.height();
pixels = defaultImage.array();
}
WritableImage image = new WritableImage(targetWidth, targetHeight);
@@ -195,6 +201,7 @@ 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'};
@@ -206,12 +213,272 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
/// ASCII local name of an SVG document element.
private static final String SVG_ELEMENT_NAME = "svg";
// This is currently a simple check, more complex checks can be considered in the future
/// 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`
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
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';
}
private static final byte[] PNG_HEADER = {
@@ -219,18 +486,10 @@ public final class ImageUtils {
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
};
private static final class PngChunkHeader {
private record PngChunkHeader(int length, int chunkType) {
private static final int IDAT_HEADER = 0x49444154;
private static final int acTL_HEADER = 0x6163544c;
private final int length;
private final int chunkType;
private PngChunkHeader(int length, int chunkType) {
this.length = length;
this.chunkType = chunkType;
}
private static @Nullable PngChunkHeader readHeader(ByteBuffer headerBuffer) {
if (headerBuffer.remaining() < 8)
return null;
@@ -254,7 +513,7 @@ public final class ImageUtils {
ByteBuffer buffer = ByteBuffer.wrap(headerBuffer, 8, headerBuffer.length - 8);
PngChunkHeader header;
@Nullable 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.
@@ -327,7 +586,7 @@ public final class ImageUtils {
int[] buffer = new int[Math.multiplyExact(width, height)];
for (int frameIndex = 0; frameIndex < frames.size(); frameIndex++) {
var frame = frames.get(frameIndex);
PngFrameControl control = frame.control;
PngFrameControl control = frame.control();
if (frameIndex == 0 && (
control.xOffset != 0 || control.yOffset != 0
@@ -347,7 +606,7 @@ public final class ImageUtils {
int[] currentFrameBuffer = buffer.clone();
if (control.blendOp == 0) {
for (int row = 0; row < control.height; row++) {
System.arraycopy(frame.bitmap.array,
System.arraycopy(frame.bitmap().array(),
row * control.width,
currentFrameBuffer,
(control.yOffset + row) * width + control.xOffset,
@@ -360,7 +619,7 @@ public final class ImageUtils {
int srcIndex = row * control.width + col;
int dstIndex = (control.yOffset + row) * width + control.xOffset + col;
int srcPixel = frame.bitmap.array[srcIndex];
int srcPixel = frame.bitmap().array()[srcIndex];
int dstPixel = currentFrameBuffer[dstIndex];
int srcAlpha = (srcPixel >>> 24) & 0xFF;
@@ -441,10 +700,10 @@ public final class ImageUtils {
}
}
PngAnimationControl animationControl = sequence.getAnimationControl();
@Nullable PngAnimationControl animationControl = sequence.getAnimationControl();
int cycleCount;
if (animationControl != null) {
cycleCount = animationControl.numPlays;
cycleCount = animationControl.numPlays();
if (cycleCount == 0)
cycleCount = Timeline.INDEFINITE;
} else {
@@ -470,6 +729,7 @@ public final class ImageUtils {
return argb;
}
/// Prevents instantiation.
private ImageUtils() {
}
}
@@ -9,22 +9,12 @@ package org.jackhuang.hmcl.ui.image.apng.argb8888;
* to be compatible with the input array used to build Android Bitmap objects, though of course
* its use is not limited to that.
*/
public final class Argb8888Bitmap {
public final int[] array;
public final int width;
public final int height;
public record Argb8888Bitmap(int[] array, int width, int height) {
public Argb8888Bitmap(int width, int height) {
this(new int[width * height], width, height);
}
public Argb8888Bitmap(int[] array, int width, int height) {//}, int x, int y) {
this.array = array;
this.width = width;
this.height = height;
// this.x = y;
}
/**
* Create a new Bitmap that shares the byte array of this bitmap.
* This is useful when rendering a number of animation frames from an APNG file
@@ -37,7 +27,7 @@ public final class Argb8888Bitmap {
* from the original bitmap).
* @return new bitmap object sharing the same data array.
*/
public Argb8888Bitmap makeView(int width, int height) {//}, int x, int y) {
public Argb8888Bitmap makeView(int width, int height) {
if ((width * height) > (this.width * this.height)) {
throw new IllegalArgumentException(String.format(
"Requested width and height (%d x %d) exceeds maximum pixels allowed by host bitmap (%d x %d",
@@ -37,7 +37,7 @@ public final class Argb8888BitmapSequence {
public void receiveAnimationControl(PngAnimationControl animationControl) {
this.animationControl = animationControl;
this.animationFrames = new ArrayList<>(animationControl.numFrames);
this.animationFrames = new ArrayList<>(animationControl.numFrames());
}
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
@@ -49,7 +49,7 @@ public final class Argb8888BitmapSequence {
}
public boolean isAnimated() {
return null != animationControl && animationControl.numFrames > 0;
return null != animationControl && animationControl.numFrames() > 0;
}
public PngAnimationControl getAnimationControl() {
@@ -60,13 +60,6 @@ public final class Argb8888BitmapSequence {
return animationFrames;
}
public static final class Frame {
public final PngFrameControl control;
public final Argb8888Bitmap bitmap;
public Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
this.control = control;
this.bitmap = bitmap;
}
public record Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
}
}
@@ -12,28 +12,21 @@ import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
* Note that the transparency of individual entries in the palette is modified in-place
* when the trNS chunk is processed.
*
* @param argbArray The colour array is public and mutable by design.
* @see BasicArgb8888Director
*/
public class Argb8888Palette {
public record Argb8888Palette(int[] argbArray) {
protected static Argb8888Palette monochromePalette = null;
protected static Argb8888Palette greyPalette2 = null;
protected static Argb8888Palette greyPalette4 = null;
protected static Argb8888Palette greyPalette8 = null;
/**
* The colour array is public and mutable by design.
*
* @see this.get()
*/
public final int[] argbArray;
/**
* Construct a new palette using the specific array passed as the argument as-is (not copied).
*
* @param argbArray array of colours to use as-is (it is not copied).
*/
public Argb8888Palette(int[] argbArray) {
this.argbArray = argbArray;
public Argb8888Palette {
}
/**
@@ -89,29 +82,33 @@ public class Argb8888Palette {
}
public static Argb8888Palette forGreyscale(int bitDepth) throws PngException {
switch (bitDepth) {
case 1:
return switch (bitDepth) {
case 1 -> {
if (null == monochromePalette) {
monochromePalette = forGreyscale(2, 0xff); // Worth it or not really?
}
return monochromePalette;
case 2:
yield monochromePalette;
}
case 2 -> {
if (null == greyPalette2) {
greyPalette2 = forGreyscale(4, 0x55);
}
return greyPalette2;
case 4:
yield greyPalette2;
}
case 4 -> {
if (null == greyPalette4) {
greyPalette4 = forGreyscale(16, 0x11);
}
return greyPalette4;
case 8: // TODO: need??
yield greyPalette4;
}
case 8 -> {
if (null == greyPalette8) {
greyPalette8 = forGreyscale(256, 0x01);
}
return greyPalette8;
default:
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
}
yield greyPalette8;
}
default ->
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
};
}
}
@@ -122,8 +122,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
int writePosition = this.y * width;
int lastWritePosition = writePosition + width;
int bit = highBit;
@@ -136,7 +136,7 @@ public class Argb8888Processors {
int dest;
if (palette != null) {
dest = palette.argbArray[index];
dest = palette.argbArray()[index];
} else {
dest = 0;
}
@@ -168,8 +168,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
@@ -177,7 +177,7 @@ public class Argb8888Processors {
int dest;
if (palette != null) {
dest = palette.argbArray[index];
dest = palette.argbArray()[index];
} else {
dest = 0;
}
@@ -203,8 +203,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int v = srcBytes[srcPosition++];
@@ -236,8 +236,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
@@ -274,8 +274,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int k = 0xff & srcBytes[srcPosition++];
@@ -303,8 +303,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
final int k = 0xff & srcBytes[srcPosition++];
@@ -333,8 +333,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
for (int x = 0; x < width; x++) {
@@ -364,8 +364,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
int writePosition = this.y * width;
//srcPosition++; // skip filter byte
for (int x = 0; x < width; x++) {
@@ -395,8 +395,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
//srcPosition++; // skip filter byte
@@ -431,8 +431,8 @@ public class Argb8888Processors {
@Override
public void processScanline(byte[] srcBytes, int srcPosition) {
final int[] destArray = this.bitmap.array;
final int width = this.bitmap.width;
final int[] destArray = this.bitmap.array();
final int width = this.bitmap.width();
//final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
int writePosition = this.y * width;
//srcPosition++; // skip filter byte
@@ -55,7 +55,7 @@ public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
public Argb8888ScanlineProcessor cloneWithNewBitmap(PngHeader header) {
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
if (this.palette != null) {
cloned.setPalette(new Argb8888Palette(palette.argbArray));
cloned.setPalette(new Argb8888Palette(palette.argbArray()));
}
return cloned;
@@ -71,7 +71,7 @@ public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
public Argb8888ScanlineProcessor cloneWithSharedBitmap(PngHeader header) {
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, bitmap.makeView(header.width, header.height));
if (this.palette != null) {
cloned.setPalette(new Argb8888Palette(palette.argbArray));
cloned.setPalette(new Argb8888Palette(palette.argbArray()));
}
return cloned;
@@ -28,7 +28,7 @@ public abstract class BasicArgb8888Director<ResultT> implements Argb8888Director
}
for (int i = 0; i < length; i++) {
final int alpha = 0xff & bytes[position + i];
palette.argbArray[i] = alpha << 24 | palette.argbArray[i] & 0x00FFFFFF;
palette.argbArray()[i] = alpha << 24 | palette.argbArray()[i] & 0x00FFFFFF;
}
}
@@ -3,25 +3,20 @@
package org.jackhuang.hmcl.ui.image.apng.chunks;
import org.jetbrains.annotations.NotNull;
/**
* A PngAnimationControl object contains data parsed from the ``acTL``
* animation control chunk of an animated PNG file.
*/
public class PngAnimationControl {
public final int numFrames;
public final int numPlays;
public PngAnimationControl(int numFrames, int numPlays) {
this.numFrames = numFrames;
this.numPlays = numPlays;
}
public record PngAnimationControl(int numFrames, int numPlays) {
public boolean loopForever() {
return 0 == numPlays;
}
@Override
public String toString() {
public @NotNull String toString() {
return "PngAnimationControl{" +
"numFrames=" + numFrames +
", numPlays=" + numPlays +
@@ -9,12 +9,7 @@ import java.io.IOException;
/**
* A PngGamma object represents data parsed from a ``gAMA`` chunk.
*/
public class PngGamma {
public final int imageGamma;
public PngGamma(int imageGamma) {
this.imageGamma = imageGamma;
}
public record PngGamma(int imageGamma) {
public static PngGamma from(DataInputStream dis) throws IOException {
return new PngGamma(dis.readInt());
@@ -49,7 +49,7 @@ public abstract class PngContainerProcessor<ResultT> implements PngChunkProcesso
@Override
public void processAnimationControl(PngAnimationControl animationControl) {
container.animationControl = animationControl;
container.animationFrames = new ArrayList<PngFrameControl>(container.animationControl.numFrames);
container.animationFrames = new ArrayList<PngFrameControl>(container.animationControl.numFrames());
}
@Override
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -136,14 +137,17 @@ public final class AboutPage extends SpinnerPane {
if (obj.has("image")) {
JsonElement image = obj.get("image");
if (image.isJsonPrimitive()) {
button.setLeading(loadImage(image.getAsString()));
var imageView = new ImageContainer(32, 32);
imageView.setImage(loadImage(image.getAsString()));
imageView.setMouseTransparent(true);
button.setLeading(imageView);
} else if (image.isJsonObject()) {
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
);
}));
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> button.setLeading(darkMode
? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
)));
}
}
@@ -21,6 +21,7 @@ import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -33,14 +34,15 @@ import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
@@ -64,10 +66,10 @@ public class DownloadSettingsPage extends StackPane {
downloadSource.getStyleClass().add("card-non-transparent");
{
Function<DownloadSource, String> converter = source -> switch (source) {
case DEFAULT -> i18n("settings.launcher.download_source.auto");
case OFFICIAL -> i18n("download.provider.official");
case MIRROR -> i18n("download.provider.mirror");
};
case DEFAULT -> i18n("settings.launcher.download_source.auto");
case OFFICIAL -> i18n("download.provider.official");
case MIRROR -> i18n("download.provider.mirror");
};
Function<DownloadSource, String> descriptionConverter = source -> {
String bundleKey = switch (source) {
case DEFAULT -> "download.provider.balanced.desc";
@@ -106,91 +108,83 @@ public class DownloadSettingsPage extends StackPane {
{
var downloadList = new ComponentList();
VBox downloadThreads = new VBox(16);
ComponentSublist fileCommonLocationSublist = new ComponentSublist(() -> {
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(settings().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
return List.of(fileCommonLocation);
});
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.descriptionProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
{
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(settings().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(settings().commonDirectoryTypeProperty());
ComponentSublist downloadThreadsSublist = new ComponentSublist(() -> {
var downloadThreadsList = new RadioChoiceList<Boolean>();
downloadThreadsList.setChoices(
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.auto"), true),
new RadioChoiceList.Choice<>(i18n("settings.launcher.download.threads.custom"), false) {
@Override
protected Node createRightNode() {
HBox hbox = new HBox(8);
hbox.setViewOrder(-1);
hbox.setAlignment(Pos.CENTER);
// hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.descriptionProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(settings().getResolvedCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it > 0)
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
.asPredicate(Validator.addTo(threadsField)));
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
downloadThreads.getChildren().add(chkAutoDownloadThreads);
var changedByTextField = new Holder<>(false);
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
changedByTextField.value = true;
slider.setValue(value.intValue());
changedByTextField.value = false;
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.value) return;
settings().downloadThreadsProperty().set(value.getValue().intValue());
});
chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
settings().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
hbox.getChildren().setAll(slider, threadsField);
return hbox;
}
}
});
);
downloadThreadsList.selectedValueProperty().bindBidirectional(settings().autoDownloadThreadsProperty());
return List.of(downloadThreadsList);
});
downloadThreadsSublist.setTitle(i18n("settings.launcher.download.threads"));
downloadThreadsSublist.descriptionProperty().bind(Bindings.createStringBinding(() -> {
if (settings().autoDownloadThreadsProperty().get()) {
return i18n("settings.launcher.download.threads.auto");
} else {
return Integer.toString(settings().downloadThreadsProperty().get());
}
}, settings().autoDownloadThreadsProperty(), settings().downloadThreadsProperty()));
{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(settings().autoDownloadThreadsProperty());
Label label = new Label(i18n("settings.launcher.download.threads"));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bind(threadsField, settings().downloadThreadsProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it > 0)
.fallbackTo(FetchTask.DEFAULT_CONCURRENCY)
.asPredicate(Validator.addTo(threadsField)));
AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(settings().downloadThreadsProperty(), value -> {
changedByTextField.set(true);
slider.setValue(value.intValue());
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
settings().downloadThreadsProperty().set(value.getValue().intValue());
});
hbox.getChildren().setAll(label, slider, threadsField);
downloadThreads.getChildren().add(hbox);
}
{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(settings().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
downloadThreads.getChildren().add(hintPane);
}
}
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreads);
downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreadsSublist);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
}
@@ -232,6 +232,7 @@ public final class JavaDownloadDialog extends StackPane {
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
this.packageTypeBox = new JFXComboBox<>(FXCollections.observableArrayList());
this.packageTypeBox.setConverter(FXUtils.stringConverter(JavaPackageType::getDisplayName));
this.downloadButton = new JFXButton(i18n("download"));
downloadButton.setOnAction(e -> onDownload());
@@ -371,7 +372,7 @@ public final class JavaDownloadDialog extends StackPane {
if (version == null)
return;
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri()))
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().pkgInfoUri()))
.setExecutor(Schedulers.io())
.thenComposeAsync(json -> {
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
@@ -379,20 +380,20 @@ public final class JavaDownloadDialog extends StackPane {
throw new IOException("Illegal result: " + json);
DiscoRemoteFileInfo fileInfo = result.getResult().get(0);
if (StringUtils.isNotBlank(fileInfo.getChecksumType())
&& !fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256") && !fileInfo.getChecksumType().equals("md5"))
throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType());
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
if (StringUtils.isNotBlank(fileInfo.checksumType())
&& !fileInfo.checksumType().equals("sha1") && !fileInfo.checksumType().equals("sha256") && !fileInfo.checksumType().equals("md5"))
throw new IOException("Unsupported checksum type: " + fileInfo.checksumType());
if (StringUtils.isBlank(fileInfo.directDownloadUri()))
throw new IOException("Missing download URI: " + json);
Path targetFile = Files.createTempFile("hmcl-java-", "." + version.getArchiveType());
targetFile.toFile().deleteOnExit();
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum()));
else if (StringUtils.isNotBlank(fileInfo.getChecksumUri()))
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri()))
if (StringUtils.isNotBlank(fileInfo.checksum()))
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), fileInfo.checksum()));
else if (StringUtils.isNotBlank(fileInfo.checksumUri()))
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.checksumUri()))
.thenApplyAsync(checksum -> {
checksum = checksum.trim();
@@ -400,15 +401,15 @@ public final class JavaDownloadDialog extends StackPane {
if (idx > 0)
checksum = checksum.substring(0, idx);
return new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum);
return new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), checksum);
});
else
getIntegrityCheck = Task.completed(null);
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.getFileName()))
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.directDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.fileName()))
.thenSupplyAsync(() -> targetFile);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
@@ -25,6 +25,7 @@ import javafx.collections.FXCollections;
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;
@@ -226,6 +227,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
private static final class JavaItemCell extends ListCell<JavaRuntime> {
private final Node graphic;
private final Label label = new Label();
private final TwoLineListItem content;
private SVG removeIcon;
@@ -241,11 +243,17 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
label.setAlignment(Pos.CENTER);
label.setMinSize(24, 24);
label.setMaxSize(24, 24);
label.setPrefSize(24, 24);
label.setStyle("-fx-background-color: -monet-secondary-container; -fx-background-radius: 2; -fx-padding: 2; -fx-font-weight: normal; -fx-font-size: 12px;");
this.content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
BorderPane.setAlignment(content, Pos.CENTER);
center.getChildren().setAll(content);
center.getChildren().setAll(label, content);
root.setCenter(center);
HBox right = new HBox();
@@ -293,6 +301,9 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
if (empty || item == null) {
setGraphic(null);
} else {
int parsedVersion = item.getParsedVersion();
label.setText(parsedVersion >= 0 ? String.valueOf(parsedVersion) : "?");
content.setTitle((item.isJDK() ? "JDK" : "JRE") + " " + item.getVersion());
content.setSubtitle(item.getBinary().toString());
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.RotateTransition;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
@@ -29,6 +30,7 @@ import javafx.event.EventHandler;
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.Tooltip;
import javafx.scene.image.ImageView;
@@ -254,14 +256,37 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton = new JFXButton();
menuButton.getStyleClass().add("menu-button");
menuButton.setOnAction(e -> GameListPopupMenu.show(
menuButton.setOnAction(e -> {
JFXPopup popup = GameListPopupMenu.showAndGetPopup(
menuButton,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.RIGHT,
0,
-menuButton.getHeight(),
repository, versions
));
);
if (popup != null) {
Node graphic = menuButton.getGraphic();
if (graphic != null) {
if (AnimationUtils.isAnimationEnabled()) {
Duration duration = Duration.millis(200);
RotateTransition rotateOpen = new RotateTransition(duration, graphic);
rotateOpen.setToAngle(-180);
FXUtils.playAnimation(graphic, "arrow-rotation", rotateOpen);
popup.setOnHidden(windowEvent -> {
RotateTransition rotateClose = new RotateTransition(duration, graphic);
rotateClose.setToAngle(0);
FXUtils.playAnimation(graphic, "arrow-rotation", rotateClose);
});
} else {
graphic.setRotate(-180);
popup.setOnHidden(windowEvent -> graphic.setRotate(0));
}
}
}
});
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
@@ -23,6 +23,7 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -69,6 +70,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
@@ -213,6 +215,60 @@ public class PersonalizationPage extends StackPane {
refresh.invalidated(null);
}
/// Binds a line toggle button to an inheritable theme appearance setting.
private static void bindThemeAppearanceToggleButton(
LineToggleButton button,
String setting,
BooleanProperty directProperty,
Supplier<Boolean> effectiveValueSupplier) {
JFXButton inheritButton = createThemeAppearanceOverrideButton();
button.setTitleTrailing(inheritButton);
Holder<Boolean> updating = new Holder<>(false);
InvalidationListener refresh = ignored -> {
if (updating.value) {
return;
}
updating.value = true;
try {
boolean overridden = settings().getThemeAppearanceOverrides().contains(setting);
button.setSelected(overridden ? directProperty.get() : effectiveValueSupplier.get());
updateThemeAppearanceOverrideButton(inheritButton, !overridden);
} finally {
updating.value = false;
}
};
button.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (updating.value) {
return;
}
updating.value = true;
try {
directProperty.set(Boolean.TRUE.equals(newValue));
settings().getThemeAppearanceOverrides().add(setting);
updateThemeAppearanceOverrideButton(inheritButton, false);
} finally {
updating.value = false;
}
refresh.invalidated(null);
});
directProperty.addListener(refresh);
addThemeAppearanceRefreshListener(refresh);
inheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (!settings().getThemeAppearanceOverrides().contains(setting)) {
directProperty.set(button.isSelected());
settings().getThemeAppearanceOverrides().add(setting);
} else {
settings().getThemeAppearanceOverrides().remove(setting);
}
refresh.invalidated(null);
event.consume();
});
refresh.invalidated(null);
}
/// Returns the display name for the currently selected launcher theme.
private static String getSelectedThemeTitle() {
ThemeReference reference = settings().getSelectedThemeOrDefault();
@@ -294,36 +350,40 @@ public class PersonalizationPage extends StackPane {
String userName = System.getProperty("user.name").trim();
String defaultAuthorName = StringUtils.isBlank(userName) ? "Unknown" : userName;
PromptDialogPane.Builder.StringQuestion packIdQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.id"),
"")
.setPromptText(defaultPackId);
PromptDialogPane.Builder.StringQuestion packNameQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.name"),
"")
.setPromptText(defaultPackName);
PromptDialogPane.Builder.StringQuestion versionQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.version"),
"")
.setPromptText(ThemePackManager.CURRENT_THEME_PACK_VERSION);
PromptDialogPane.Builder.StringQuestion authorNameQuestion = new PromptDialogPane.Builder.StringQuestion(
i18n("theme_pack.export.author"),
"")
.setPromptText(defaultAuthorName);
Controllers.prompt(new PromptDialogPane.Builder(i18n("theme_pack.export.title"), (questions, handler) -> handler.resolve())
.addQuestion(packIdQuestion)
.addQuestion(packNameQuestion)
.addQuestion(versionQuestion)
.addQuestion(authorNameQuestion)).thenAccept(questions -> exportCurrentThemePack(
StringUtils.isBlank(packIdQuestion.getValue()) ? defaultPackId : packIdQuestion.getValue().trim(),
defaultPackId,
StringUtils.isBlank(versionQuestion.getValue())
? ThemePackManager.CURRENT_THEME_PACK_VERSION
: versionQuestion.getValue().trim(),
StringUtils.isBlank(packNameQuestion.getValue()) ? defaultPackName : packNameQuestion.getValue().trim(),
StringUtils.isBlank(authorNameQuestion.getValue()) ? defaultAuthorName : authorNameQuestion.getValue().trim()));
}
/// Saves current launcher appearance as a theme-pack file with the given package metadata.
private void exportCurrentThemePack(String packId, String packName, String authorName) {
private void exportCurrentThemePack(String packId, String version, String packName, String authorName) {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("theme_pack.export.title"));
String initialFileName = sanitizeThemePackFileName(packName) + ThemePackExporter.FILE_EXTENSION;
chooser.setInitialFileName(FileUtils.isNameValid(initialFileName)
? initialFileName
: "theme-pack" + ThemePackExporter.FILE_EXTENSION);
String initialFileName = sanitizeThemePackFileName(packName);
chooser.setInitialFileName(
(FileUtils.isNameValid(initialFileName) ? initialFileName : "theme-pack")
+ (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ? "" : ThemePackExporter.FILE_EXTENSION)
);
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter(i18n("theme_pack.file"), "*" + ThemePackExporter.FILE_EXTENSION));
@@ -340,6 +400,7 @@ public class PersonalizationPage extends StackPane {
ThemePackManager.exportCurrent(
output,
packId,
version,
packName,
authorName);
Controllers.dialog(
@@ -439,6 +500,10 @@ public class PersonalizationPage extends StackPane {
i18n("settings.launcher.theme_color_type.default"),
ThemeColorType.DEFAULT);
var systemColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
i18n("settings.launcher.theme_color_type.system"),
ThemeColorType.SYSTEM);
var customColorChoice = new RadioChoiceList.Choice<ThemeColorType>(
i18n("settings.launcher.theme_color_type.custom"),
ThemeColorType.CUSTOM) {
@@ -455,7 +520,11 @@ public class PersonalizationPage extends StackPane {
RadioChoiceList<ThemeColorType> themeColorChoiceList = new RadioChoiceList<>();
themeColorChoiceList.setFallbackValue(ThemeColorType.DEFAULT);
themeColorChoiceList.setChoices(Arrays.asList(defaultColorChoice, customColorChoice, backgroundColorChoice));
themeColorChoiceList.setChoices(Arrays.asList(
defaultColorChoice,
systemColorChoice,
customColorChoice,
backgroundColorChoice));
JFXButton themeColorOverrideButton = createThemeAppearanceOverrideButton();
themeColorSublist.setTitleRight(themeColorOverrideButton);
@@ -1072,6 +1141,25 @@ public class PersonalizationPage extends StackPane {
refresh.invalidated(null);
themeAppearanceList.getContent().add(titleBarTransparentButton);
}
LineToggleButton windowTransparentButton = new LineToggleButton();
windowTransparentButton.setTitle(i18n("settings.launcher.window_transparent"));
bindThemeAppearanceToggleButton(
windowTransparentButton,
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT,
settings().windowTransparentProperty(),
() -> {
try {
return Objects.requireNonNullElse(
ThemePackManager.resolveCurrentWindowTransparent(
ThemePackManager.currentResolveContext()),
false);
} catch (IOException | RuntimeException e) {
return false;
}
});
themeAppearanceList.getContent().add(windowTransparentButton);
content.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")),
themeAppearanceList,
@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.AprilFools;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -195,7 +194,7 @@ public final class SettingsPage extends ScrollPane {
{
ComponentList miscPaneList = new ComponentList();
if (AprilFools.isShowAprilFoolsSettings()) {
{
LineToggleButton disableAprilFools = new LineToggleButton();
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
@@ -32,8 +32,7 @@ import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow;
@@ -550,17 +549,10 @@ public class TerracottaControllerPage extends StackPane {
private ComponentSublist getThirdPartyDownloadNodes() {
ComponentSublist locals = new ComponentSublist();
var header = new LinePane();
header.getStyleClass().add("no-padding");
header.setLargeTitle(true);
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
HBox.setHgrow(header, Priority.ALWAYS);
header.setMouseTransparent(true);
header.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
header.setTitle(i18n("terracotta.from_local.title"));
header.setSubtitle(i18n("terracotta.from_local.desc"));
locals.setHeaderLeft(header);
locals.setLargeTitle(true);
locals.setLeading(new ImageView(FXUtils.newBuiltinImage("/assets/img/terracotta.png")));
locals.setTitle(i18n("terracotta.from_local.title"));
locals.setSubtitle(i18n("terracotta.from_local.desc"));
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
LineButton item = new LineButton();
@@ -514,15 +514,23 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
pane.setCenter(spinnerPane);
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
spinnerPane.failedReasonProperty().bind(
Bindings.createStringBinding(() -> {
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");
} else {
return null;
}
},
getSkinnable().failedProperty(),
getSkinnable().loadingProperty(),
getSkinnable().pageCount,
getSkinnable().items)
);
spinnerPane.setOnFailedAction(e -> {
if (getSkinnable().retrySearch != null) {
if (getSkinnable().isFailed() && getSkinnable().retrySearch != null) {
getSkinnable().retrySearch.run();
}
});
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.setting.VersionIconType;
@@ -35,25 +37,35 @@ public class GameAdvancedListItem extends AdvancedListItem {
private final ImageContainer imageContainer;
private final WeakListenerHolder holder = new WeakListenerHolder();
private HMCLGameRepository repository;
@SuppressWarnings("unused")
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private Consumer<Event> onVersionIconChangedListener;
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private Consumer<RefreshedVersionsEvent> onRefreshedVersionsListener;
public GameAdvancedListItem() {
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
imageContainer.setMouseTransparent(true);
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
setLeftGraphic(imageContainer);
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadVersion));
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), it -> this.loadVersion()));
}
private void loadVersion(String version) {
if (GameDirectoryManager.getSelectedRepository() != repository) {
private void loadVersion() {
String version = GameDirectoryManager.getSelectedInstance();
boolean repositoryChanged = GameDirectoryManager.getSelectedRepository() != repository;
if (repositoryChanged) {
repository = GameDirectoryManager.getSelectedRepository();
if (repository != null) {
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
this.loadVersion(repository.getSelectedInstance());
});
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
FXUtils.runInFX(this::loadVersion);
});
if (!repository.isLoaded()) {
onRefreshedVersionsListener = EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
.registerWeak(event -> FXUtils.runInFX(this::loadVersion));
return;
}
}
if (version != null && repository != null && repository.hasVersion(version)) {
@@ -80,6 +80,7 @@ public final class GameListCell extends ListCell<GameListItem> {
{
HBox center = new HBox();
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
center.setMouseTransparent(true);
root.setCenter(center);
center.setPrefWidth(Region.USE_PREF_SIZE);
@@ -51,6 +51,12 @@ public final class GameListPopupMenu extends StackPane {
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
HMCLGameRepository repository, List<Version> versions) {
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, repository, versions);
}
public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
HMCLGameRepository repository, List<Version> versions) {
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream()
.filter(it -> repository.hasVersion(it.getId()))
@@ -58,6 +64,8 @@ public final class GameListPopupMenu extends StackPane {
.toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
return popup;
}
private final JFXListView<GameItem> listView = new JFXListView<>();
@@ -163,7 +163,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
// Sinytra Connector
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
@@ -172,7 +172,7 @@ 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", "*.litemod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res == null) return;
@@ -328,46 +328,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
iconPaths.add(this.localModFile.getLogoPath());
}
iconPaths.addAll(List.of(
"icon.png",
"logo.png",
"mod_logo.png",
"pack.png",
"logoFile.png",
"assets/icon.png",
"assets/logo.png",
"assets/mod_icon.png",
"assets/mod_logo.png",
"META-INF/icon.png",
"META-INF/logo.png",
"META-INF/mod_icon.png",
"textures/icon.png",
"textures/logo.png",
"textures/mod_icon.png",
"resources/icon.png",
"resources/logo.png",
"resources/mod_icon.png"
));
String modId = this.localModFile.getId();
if (StringUtils.isNotBlank(modId)) {
iconPaths.addAll(List.of(
"assets/" + modId + "/icon.png",
"assets/" + modId + "/logo.png",
"assets/" + modId.replace("-", "") + "/icon.png",
"assets/" + modId.replace("-", "") + "/logo.png",
modId + ".png",
modId + "-logo.png",
modId + "-icon.png",
modId + "_logo.png",
modId + "_icon.png",
"textures/" + modId + "/icon.png",
"textures/" + modId + "/logo.png",
"resources/" + modId + "/icon.png",
"resources/" + modId + "/logo.png"
));
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
for (String path : iconPaths) {
Path iconPath = fs.getPath(path);
@@ -562,7 +522,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(24);
ImageContainer imageContainer = new ImageContainer(32);
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
@@ -471,7 +471,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
private final ResourcePackListPage page;
private final JFXCheckBox checkBox;
private final ImageContainer imageContainer = new ImageContainer(24);
private final ImageContainer imageContainer = new ImageContainer(32);
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER);
private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO);
@@ -314,8 +314,13 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
@Override
protected void updateItem(World world, boolean empty) {
World oldWorld = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(world, empty);
if (oldWorld == world && oldEmpty == empty) return;
this.content.getTags().clear();
if (empty || world == null) {
@@ -34,7 +34,6 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings;
public final class AprilFools {
private static final boolean ENABLED;
private static final boolean SHOW_APRIL_FOOLS_SETTINGS;
static {
var date = LocalDate.now();
@@ -58,14 +57,6 @@ public final class AprilFools {
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;
ENABLED = aprilFoolsMode && !settings().disableAprilFoolsProperty().get();
SHOW_APRIL_FOOLS_SETTINGS = aprilFoolsMode || supportedRegion && date.getMonth() == Month.MARCH && date.getDayOfMonth() > 30;
}
/// Whether April Fools settings should be shown.
///
/// This method returns true if April Fools settings should be shown.
public static boolean isShowAprilFoolsSettings() {
return SHOW_APRIL_FOOLS_SETTINGS;
}
/// Whether April Fools is enabled.
@@ -78,7 +78,7 @@ public final class NativePatcher {
JavaRuntime javaVersion,
GameSettings.Effective settings,
List<String> javaArguments) {
if (settings.get(GameSettings::useCustomNativesProperty)) {
if (settings.getInheritable(GameSettings::useCustomNativesProperty)) {
if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0)
return version;
@@ -96,8 +96,8 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
final boolean useNativeGLFW = settings.get(GameSettings::useNativeGLFWProperty);
final boolean useNativeOpenAL = settings.get(GameSettings::useNativeOpenALProperty);
final boolean useNativeGLFW = settings.getInheritable(GameSettings::useNativeGLFWProperty);
final boolean useNativeOpenAL = settings.getInheritable(GameSettings::useNativeOpenALProperty);
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && (useNativeGLFW || useNativeOpenAL)
&& gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") >= 0) {
@@ -124,7 +124,7 @@ public final class NativePatcher {
Architecture arch = javaVersion.getArchitecture();
GameVersionNumber gameVersionNumber = gameVersion != null ? GameVersionNumber.asGameVersion(gameVersion) : null;
if (settings.get(GameSettings::notPatchNativesProperty))
if (settings.getInheritable(GameSettings::notPatchNativesProperty))
return version;
if (arch.isX86() && (os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.MACOS))
@@ -7,14 +7,17 @@
}
.log-window {
-fixed-log-background: #282828;
-fixed-log-toggle-selected: #DEE2E6;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #FFFFFF;
-fixed-log-trace: #495057;
-fixed-log-debug: #343A40;
-fixed-log-info: #212529;
-fixed-log-warn: #331904;
-fixed-log-error: #58151C;
-fixed-log-fatal: #842029;
-fixed-log-selected: #6C757D;
-fixed-log-text-fill: #e3e3e3;
-fixed-log-trace-background: #495057;
-fixed-log-debug-background: #343A40;
-fixed-log-info-background: #212529;
-fixed-log-warn-background: #413c26;
-fixed-log-warn-text-fill: #fdf3aa;
-fixed-log-error-background: #4e3534;
-fixed-log-error-text-fill: #f9dedc;
-fixed-log-fatal-background: #842029;
-fixed-log-selected: #3e3e3e;
}
@@ -7,14 +7,17 @@
}
.log-window {
-fixed-log-background: #ffffff;
-fixed-log-toggle-selected: #000000;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #000000;
-fixed-log-trace: #EEE9E0;
-fixed-log-debug: #EEE9E0;
-fixed-log-info: #FFFFFF;
-fixed-log-warn: #FFEECC;
-fixed-log-error: #FFCCBB;
-fixed-log-fatal: #F7A699;
-fixed-log-selected: #C4C4C4;
-fixed-log-text-fill: #1f1f1f;
-fixed-log-trace-background: #EEE9E0;
-fixed-log-debug-background: #EEE9E0;
-fixed-log-info-background: #FFFFFF;
-fixed-log-warn-background: #fef6d5;
-fixed-log-warn-text-fill: #3e2f00;
-fixed-log-error-background: #fcebeb;
-fixed-log-error-text-fill: #410e0b;
-fixed-log-fatal-background: #F7A699;
-fixed-log-selected: #f2f2f2;
}
+34 -22
View File
@@ -396,6 +396,10 @@
-fx-cursor: hand;
}
.installer-item-status:incompatible {
-fx-opacity: 0.6;
}
.installer-item-wrapper {
-fx-background-color: -monet-surface;
-fx-background-radius: 4;
@@ -448,16 +452,24 @@
}
.launch-pane > .jfx-button.launch-button {
-fx-max-width: 207px;
-fx-min-width: 207px;
-fx-max-width: 200px;
-fx-min-width: 200px;
-fx-border-width: 0 3px 0 0;
-fx-border-color: -monet-on-surface-variant;
-fx-border-color: linear-gradient(
to bottom,
transparent 0%,
transparent 20%,
-monet-on-surface-variant 20%,
-monet-on-surface-variant 80%,
transparent 80%,
transparent 100%
);
-fx-background-radius: 4px 0 0 4px;
}
.launch-pane > .jfx-button.menu-button {
-fx-max-width: 20px;
-fx-min-width: 20px;
-fx-max-width: 27px;
-fx-min-width: 27px;
-fx-font-size: 15px;
-fx-background-radius: 0 4px 4px 0;
}
@@ -1382,27 +1394,27 @@
}
.log-toggle.fatal {
-fx-background-color: -fixed-log-fatal;
-fx-background-color: -fixed-log-fatal-background;
}
.log-toggle.error {
-fx-background-color: -fixed-log-error;
-fx-background-color: -fixed-log-error-background;
}
.log-toggle.warn {
-fx-background-color: -fixed-log-warn;
-fx-background-color: -fixed-log-warn-background;
}
.log-toggle.info {
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-toggle.debug {
-fx-background-color: -fixed-log-debug;
-fx-background-color: -fixed-log-debug-background;
}
.log-toggle.trace {
-fx-background-color: -fixed-log-trace;
-fx-background-color: -fixed-log-trace-background;
}
.log-window-list-cell:selected {
@@ -1410,7 +1422,7 @@
}
.log-window {
-fx-background-color: -monet-surface-container;
-fx-background-color: -fixed-log-background;
}
.log-window .scroll-bar .thumb {
@@ -1433,36 +1445,36 @@
.log-window-list-cell:fatal {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-fatal;
-fx-background-color: -fixed-log-fatal-background;
}
.log-window-list-cell:error {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-error;
-fx-text-fill: -fixed-log-error-text-fill;
-fx-background-color: -fixed-log-error-background;
}
.log-window-list-cell:warn {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-warn;
-fx-text-fill: -fixed-log-warn-text-fill;
-fx-background-color: -fixed-log-warn-background;
}
.log-window-list-cell:info {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-window-list-cell {
-fx-background-color: -fixed-log-info;
-fx-background-color: -fixed-log-info-background;
}
.log-window-list-cell:debug {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-debug;
-fx-background-color: -fixed-log-debug-background;
}
.log-window-list-cell:trace {
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-trace;
-fx-background-color: -fixed-log-trace-background;
}
.log-window-list-cell:selected {
@@ -1581,7 +1593,7 @@
-fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
-fx-background-radius: 3px;
-fx-background-insets: 0px;
-fx-min-height: 25px;
-fx-min-height: 36px;
}
.color-palette-region {
@@ -399,6 +399,7 @@ 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
@@ -487,7 +488,7 @@ fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive,
fatal.illegal_char=Your working directory contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
\n\
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" or "Mesa Lavapipe" in "Global/Instance-specific Settings → Graphics Settings" to use CPU rendering for better compatibility.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
@@ -1374,6 +1375,7 @@ settings.advanced.game_dir.default=Default (".minecraft/")
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
settings.advanced.graphics=Graphics Settings
settings.advanced.graphics_backend=Graphics API
settings.advanced.graphics_backend.desc=Only effective for Minecraft 26.2+
settings.advanced.graphics_backend.default=Default
settings.advanced.graphics_backend.default.desc=Follow the game's settings
settings.advanced.graphics_backend.opengl=OpenGL
@@ -1534,7 +1536,7 @@ settings.game_directories.read_only=The game directory file was saved by a newer
settings.launcher=Launcher Settings
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience
settings.launcher.animation=Animation
settings.launcher.appearance=Appearance
settings.launcher.brightness=Theme Mode
@@ -1546,8 +1548,9 @@ settings.launcher.debug=Debug
settings.launcher.disable_april_fools=Do not enable April Fools features
settings.launcher.disable_auto_game_options=Do not switch game language
settings.launcher.download=Download
settings.launcher.download.threads=Threads
settings.launcher.download.threads=Download Threads
settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.custom=Custom
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
@@ -1603,16 +1606,16 @@ settings.launcher.theme_color_type.background=Follow Background Wallpaper
settings.launcher.theme_color_type.custom=Custom Color
settings.launcher.theme_color_type.custom.description=Use the color selected above.
settings.launcher.theme_color_type.default=Default
settings.launcher.theme_color_type.system=Follow System
settings.launcher.title_transparent=Transparent Titlebar
settings.launcher.turn_off_animations=Disable Animation
settings.launcher.version_list_source=Version List
settings.launcher.window_transparent=Transparent Window
settings.launcher.background.settings.opacity=Opacity
settings.memory=Memory
settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated
settings.memory.allocate.auto.exceeded=%1$.1f GiB Minimum / %2$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.allocate.manual=%1$.1f GiB Allocated
settings.memory.allocate.manual.exceeded=%1$.1f GiB Allocated (%3$.1f GiB Available)
settings.memory.allocate=%1$.1f GiB Allocated
settings.memory.allocate.exceeded=%1$.1f GiB Allocated (%2$.1f GiB Available)
settings.memory.auto_allocate=Automatically Allocate
settings.memory.lower_bound=Minimum Memory
settings.memory.manual_allocate=Manually Allocate Memory
@@ -1631,11 +1634,14 @@ settings.type.global.preset=Preset
settings.type.global.preset.create=Create Preset
settings.type.global.preset.default=Use Default Preset
settings.type.global.preset.duplicate_name=Preset name already exists
settings.type.global.preset.manage_all=Default Preset
settings.type.global.preset.help=A game settings preset manages a set of game settings that you can adjust on this page.\n\
All instances inherit game settings from the current preset by default. You can override specific settings on the instance game settings page.
settings.type.global.preset.manage_all=Current Preset
settings.type.global.preset.name=Preset Name
settings.type.global.preset.rename=Rename Preset
settings.type.global.preset.remove=Delete Preset
settings.type.global.preset.remove.confirm=Are you sure you want to delete preset "%s"? Instances using this preset will fall back to the default preset.
settings.type.global.preset.subtitle=The current instance will inherit game settings from the specified preset
settings.type.global.preset.auto_name=Preset %d
shaderpack.download.title=Download Shader - %1s
@@ -1749,6 +1755,7 @@ theme_pack.export.id=Theme Pack ID
theme_pack.export.name=Theme Name
theme_pack.export.subtitle=Save the current launcher appearance as a theme-pack file.
theme_pack.export.success=Theme pack has been saved to "%s".
theme_pack.export.version=Theme Version
theme_pack.export.title=Save theme pack
theme_pack.file=HMCL Theme Pack
theme_pack.import=Import Theme Pack
@@ -1428,10 +1428,8 @@ settings.launcher.version_list_source=قائمة الإصدارات
settings.launcher.background.settings.opacity=الشفافية
settings.memory=الذاكرة
settings.memory.allocate.auto=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص
settings.memory.allocate.auto.exceeded=%1$.1f جيجابايت كحد أدنى / %2$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
settings.memory.allocate.manual=%1$.1f جيجابايت مخصص
settings.memory.allocate.manual.exceeded=%1$.1f جيجابايت مخصص (%3$.1f جيجابايت متاح)
settings.memory.allocate=%1$.1f جيجابايت مخصص
settings.memory.allocate.exceeded=%1$.1f جيجابايت مخصص (%2$.1f جيجابايت متاح)
settings.memory.auto_allocate=تخصيص تلقائي
settings.memory.lower_bound=الحد الأدنى للذاكرة
settings.memory.unit.mib=ميجابايت
@@ -1329,10 +1329,8 @@ settings.launcher.version_list_source=Lista de versiones
settings.launcher.background.settings.opacity=Opacidad
settings.memory=Memoria
settings.memory.allocate.auto=%1$.1f GiB Mínimo / %2$.1f GiB Asignado
settings.memory.allocate.auto.exceeded=%1$.1f GiB Mínimo / %2$.1f GiB Asignado (%3$.1f GiB Disponible)
settings.memory.allocate.manual=%1$.1f GiB Asignados
settings.memory.allocate.manual.exceeded=%1$.1f GiB Asignados (%3$.1f GiB Disponibles)
settings.memory.allocate=%1$.1f GiB Asignados
settings.memory.allocate.exceeded=%1$.1f GiB Asignados (%2$.1f GiB Disponibles)
settings.memory.auto_allocate=Asignar automáticamente
settings.memory.lower_bound=Memoria mínima
settings.memory.unit.mib=MiB
@@ -856,10 +856,8 @@ settings.launcher.title_transparent=タイトルの透明性
settings.launcher.version_list_source=バージョンリストソース
settings.memory=ゲームメモリ
settings.memory.allocate.auto=最小 %1$.1f GiB /実際の %2$.1f GiB
settings.memory.allocate.auto.exceeded=最小 %1$.1f GiB /実際の %2$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate.manual=割り当て %1$.1f GiB
settings.memory.allocate.manual.exceeded=割り当て %1$.1f GiB(%3$.1f GiB使用可能)
settings.memory.allocate=割り当て %1$.1f GiB
settings.memory.allocate.exceeded=割り当て %1$.1f GiB(%2$.1f GiB使用可能)
settings.memory.auto_allocate=メモリサイズの自動割り当て
settings.memory.lower_bound=最小メモリ
settings.memory.unit.mib=MiB
@@ -1110,7 +1110,7 @@ settings.icon=戲徵
settings.launcher=啟者置設
settings.launcher.allow_auto_agent=許改戲
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
settings.launcher.debug=勘誤
@@ -1152,10 +1152,8 @@ settings.launcher.version_list_source=版列供者
settings.launcher.background.settings.opacity=陰翳
settings.memory=戲憶
settings.memory.allocate.auto=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB
settings.memory.allocate.auto.exceeded=至低分之 %1$.1f GiB / 實分之 %2$.1f GiB (械祇 %3$.1f GiB)
settings.memory.allocate.manual=戲分之 %1$.1f GiB
settings.memory.allocate.manual.exceeded=戲分之 %1$.1f GiB (械祇 %3$.1f GiB)
settings.memory.allocate=戲分之 %1$.1f GiB
settings.memory.allocate.exceeded=戲分之 %1$.1f GiB (械祇 %2$.1f GiB)
settings.memory.auto_allocate=自調其憶
settings.memory.lower_bound=至低憶界
settings.memory.unit.mib=兆二進字節
@@ -1318,10 +1318,8 @@ settings.launcher.version_list_source=Список версий
settings.launcher.background.settings.opacity=Непрозрачность
settings.memory=Оперативная память
settings.memory.allocate.auto=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ
settings.memory.allocate.auto.exceeded=Минимум %1$.1f ГиБ / Выделено %2$.1f ГиБ (Доступно %3$.1f ГиБ)
settings.memory.allocate.manual=Выделено %1$.1f ГиБ
settings.memory.allocate.manual.exceeded=Выделено %1$.1f ГиБ (Доступно %3$.1f ГиБ)
settings.memory.allocate=Выделено %1$.1f ГиБ
settings.memory.allocate.exceeded=Выделено %1$.1f ГиБ (Доступно %2$.1f ГиБ)
settings.memory.auto_allocate=Автовыделение
settings.memory.lower_bound=Мин. объём памяти
settings.memory.unit.mib=МиБ
@@ -1267,10 +1267,8 @@ settings.launcher.version_list_source=Список версій
settings.launcher.background.settings.opacity=Непрозорість
settings.memory=Пам'ять
settings.memory.allocate.auto=%1$.1f ГіБ Мінімум / %2$.1f ГіБ Виділено
settings.memory.allocate.auto.exceeded=%1$.1f ГіБ Мінімум / %2$.1f ГіБ Виділено (%3$.1f ГіБ Доступно)
settings.memory.allocate.manual=%1$.1f ГіБ Виділено
settings.memory.allocate.manual.exceeded=%1$.1f ГіБ Виділено (%3$.1f ГіБ Доступно)
settings.memory.allocate=%1$.1f ГіБ Виділено
settings.memory.allocate.exceeded=%1$.1f ГіБ Виділено (%2$.1f ГіБ Доступно)
settings.memory.auto_allocate=Автоматично виділяти
settings.memory.lower_bound=Мінімальна пам'ять
settings.memory.unit.mib=МіБ
@@ -397,6 +397,7 @@ 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
@@ -457,7 +458,7 @@ fatal.deprecated_java_version.download_link=下載 Java %d
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 圖形設定」中將「繪製器」切換為「Mesa LLVMpipe」/「Mesa Lavapipe」,以獲得更好的相容性。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
# When translating the following string into non-Latin languages, please retain the English translation
@@ -1167,6 +1168,7 @@ settings.advanced.game_dir.default=預設 (".minecraft/")
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
settings.advanced.graphics=圖形設定
settings.advanced.graphics_backend=圖形 API
settings.advanced.graphics_backend.desc=僅對 Minecraft 26.2+ 生效
settings.advanced.graphics_backend.default=預設
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
settings.advanced.graphics_backend.opengl=OpenGL
@@ -1292,7 +1294,7 @@ settings.game.inherit_global=目前繼承遊戲設定預設。點擊後使用實
settings.game.instance_settings.unsupported=目前遊戲設定檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法正常載入。
settings.game.presets.unsupported=遊戲設定預設檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改全域遊戲設定。
settings.game.isolation=版本隔離
settings.game.isolation.subtitle=啟用後,目前實例將使用獨立的遊戲執行路徑設定,而不是繼承全域設定。
settings.game.isolation.subtitle=啟用後,目前實例將使用獨立的遊戲執行路徑設定,而不是繼承全域設定
settings.game.java_directory=遊戲 Java
settings.game.java_directory.auto=自動選取合適的 Java
settings.game.java_directory.auto.not_found=沒有合適的 Java
@@ -1325,7 +1327,7 @@ settings.game_directories.read_only=遊戲目錄設定檔是由更高版本的 H
settings.launcher=啟動器設定
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗。
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗
settings.launcher.animation=动画
settings.launcher.appearance=外觀
settings.launcher.brightness=主題模式
@@ -1337,12 +1339,13 @@ settings.launcher.debug=除錯
settings.launcher.disable_april_fools=不啟用愚人節功能
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
settings.launcher.download=下載
settings.launcher.download.threads=執行緒數
settings.launcher.download.threads=下載執行緒數
settings.launcher.download.threads.auto=自動選取執行緒數
settings.launcher.download.threads.custom=自訂執行緒數
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.default_addon_source=遊戲內容預設下載來源
settings.launcher.default_addon_source=附加內容預設下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.misc=雜項
settings.launcher.font=字體
@@ -1394,16 +1397,16 @@ settings.launcher.theme_color_type.background=跟隨背景圖片
settings.launcher.theme_color_type.custom=自訂顏色
settings.launcher.theme_color_type.custom.description=使用上方選擇的顏色。
settings.launcher.theme_color_type.default=預設
settings.launcher.theme_color_type.system=跟隨系統
settings.launcher.title_transparent=標題欄透明
settings.launcher.turn_off_animations=關閉動畫
settings.launcher.version_list_source=版本清單來源
settings.launcher.window_transparent=視窗透明
settings.launcher.background.settings.opacity=不透明度
settings.memory=遊戲記憶體
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB
settings.memory.allocate.auto.exceeded=最低分配 %1$.1f GiB / 實際分配 %2$.1f GiB (%3$.1f GiB 可用)
settings.memory.allocate.manual=遊戲分配 %1$.1f GiB
settings.memory.allocate.manual.exceeded=遊戲分配 %1$.1f GiB (%3$.1f GiB 可用)
settings.memory.allocate=遊戲分配 %1$.1f GiB
settings.memory.allocate.exceeded=遊戲分配 %1$.1f GiB (%2$.1f GiB 可用)
settings.memory.auto_allocate=自動分配
settings.memory.lower_bound=最低分配
settings.memory.manual_allocate=手動選取記憶體
@@ -1422,11 +1425,14 @@ settings.type.global.preset=預設
settings.type.global.preset.create=新增預設
settings.type.global.preset.default=使用預設項目
settings.type.global.preset.duplicate_name=預設名稱已存在
settings.type.global.preset.manage_all=預設項目
settings.type.global.preset.help=遊戲設定預設管理著一組遊戲設定,你可以在下方調整這些設定。\n\
所有實例預設繼承當前預設中的遊戲設定,你可以在實例遊戲設定頁面中覆蓋特定的設定。
settings.type.global.preset.manage_all=當前預設
settings.type.global.preset.name=預設名稱
settings.type.global.preset.rename=重新命名預設
settings.type.global.preset.remove=刪除預設
settings.type.global.preset.remove.confirm=確定要刪除預設「%s」嗎?使用該預設的實例將改用預設項目。
settings.type.global.preset.subtitle=當前實例將繼承指定預設中的遊戲設定
settings.type.global.preset.auto_name=預設 %d
shaderpack.download.title=光影下載 - %1s
@@ -1540,6 +1546,7 @@ theme_pack.export.id=主題包 ID
theme_pack.export.name=主題名稱
theme_pack.export.subtitle=將目前啟動器外觀儲存為主題包檔案
theme_pack.export.success=主題包已儲存到「%s」
theme_pack.export.version=主題版本
theme_pack.export.title=儲存主題包
theme_pack.file=HMCL 主題包
theme_pack.import=匯入主題包
@@ -399,6 +399,7 @@ 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
@@ -460,7 +461,7 @@ fatal.deprecated_java_version.download_link=下载 Java %d
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.illegal_char=由于你的运行路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n\
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\n\
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 图形设置”中将“渲染器”切换为“Mesa LLVMpipe” / “Mesa Lavapipe”,以获得更好的兼容性。\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -1172,6 +1173,7 @@ settings.advanced.game_dir.default=默认 (".minecraft/")
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
settings.advanced.graphics=图形设置
settings.advanced.graphics_backend=图形 API
settings.advanced.graphics_backend.desc=仅对 Minecraft 26.2+ 生效
settings.advanced.graphics_backend.default=默认
settings.advanced.graphics_backend.default.desc=遵循游戏设置
settings.advanced.graphics_backend.opengl=OpenGL
@@ -1297,7 +1299,7 @@ settings.game.inherit_global=当前继承游戏设置预设。点击后使用实
settings.game.instance_settings.unsupported=当前游戏设置文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法正常加载。
settings.game.presets.unsupported=游戏设置预设文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改全局游戏设置。
settings.game.isolation=版本隔离
settings.game.isolation.subtitle=启用后,当前实例将使用独立的游戏运行路径设置,而不是继承全局设置。
settings.game.isolation.subtitle=启用后,当前实例将使用独立的游戏运行路径设置,而不是继承全局设置
settings.game.java_directory=游戏 Java
settings.game.java_directory.auto=自动选择合适的 Java
settings.game.java_directory.auto.not_found=没有合适的 Java
@@ -1330,7 +1332,7 @@ settings.game_directories.read_only=游戏文件夹配置文件是由更高版
settings.launcher=启动器设置
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验。
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验
settings.launcher.animation=动画
settings.launcher.appearance=外观
settings.launcher.brightness=主题模式
@@ -1342,12 +1344,13 @@ settings.launcher.debug=调试
settings.launcher.disable_april_fools=不启用愚人节功能
settings.launcher.disable_auto_game_options=不自动切换游戏语言
settings.launcher.download=下载
settings.launcher.download.threads=线程数
settings.launcher.download.threads=下载线程数
settings.launcher.download.threads.auto=自动选择线程数
settings.launcher.download.threads.custom=自定义线程数
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.default_addon_source=游戏内容默认下载源
settings.launcher.default_addon_source=附加内容默认下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.misc=杂项
settings.launcher.font=启动器字体
@@ -1399,16 +1402,16 @@ settings.launcher.theme_color_type.background=跟随背景壁纸
settings.launcher.theme_color_type.custom=自定义颜色
settings.launcher.theme_color_type.custom.description=使用上方选择的颜色。
settings.launcher.theme_color_type.default=默认
settings.launcher.theme_color_type.system=跟随系统
settings.launcher.title_transparent=标题栏透明
settings.launcher.turn_off_animations=关闭动画
settings.launcher.version_list_source=版本列表源
settings.launcher.window_transparent=窗口透明
settings.launcher.background.settings.opacity=不透明度
settings.memory=游戏内存
settings.memory.allocate.auto=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB
settings.memory.allocate.auto.exceeded=最低分配 %1$.1f GiB / 实际分配 %2$.1f GiB (%3$.1f GiB 可用)
settings.memory.allocate.manual=游戏分配 %1$.1f GiB
settings.memory.allocate.manual.exceeded=游戏分配 %1$.1f GiB (设备仅 %3$.1f GiB 可用)
settings.memory.allocate=游戏分配 %1$.1f GiB
settings.memory.allocate.exceeded=游戏分配 %1$.1f GiB (设备仅 %2$.1f GiB 可用)
settings.memory.auto_allocate=自动分配内存
settings.memory.lower_bound=最低内存分配
settings.memory.manual_allocate=手动选择内存
@@ -1427,11 +1430,14 @@ settings.type.global.preset=预设
settings.type.global.preset.create=新建预设
settings.type.global.preset.default=使用默认预设
settings.type.global.preset.duplicate_name=预设名称已存在
settings.type.global.preset.manage_all=默认预设
settings.type.global.preset.help=游戏设置预设管理着一组游戏设置,你可以在下方调整这些设置。\n\
所有实例默认继承当前预设中的游戏设置,你可以在实例游戏设置页面中覆盖特定的设置。
settings.type.global.preset.manage_all=当前预设
settings.type.global.preset.name=预设名称
settings.type.global.preset.rename=重命名预设
settings.type.global.preset.remove=删除预设
settings.type.global.preset.remove.confirm=确定要删除预设“%s”吗?使用该预设的实例将回退到默认预设。
settings.type.global.preset.subtitle=当前实例将继承指定预设中的游戏设置
settings.type.global.preset.auto_name=预设 %d
shaderpack.download.title=光影下载 - %1s
@@ -1545,6 +1551,7 @@ theme_pack.export.id=主题包 ID
theme_pack.export.name=主题名称
theme_pack.export.subtitle=将当前启动器外观保存为主题包文件
theme_pack.export.success=主题包已保存到“%s”
theme_pack.export.version=主题版本
theme_pack.export.title=保存主题包
theme_pack.file=HMCL 主题包
theme_pack.import=导入主题包
File diff suppressed because it is too large. Load diff
+140 -36
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 Vanilla Flavoured;;
slightly-vanilla-flavoured;149;;Slightly Flavoured / 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;;
;212;;TeaCon整合包;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;;月见苍穹传说;TskimiCanopyCraft;TSCC
;560;;月见苍穹传说;TsukiCupolaCraft;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;;塔可努的原生优化整合包;TechNoob's Vanilla Boost Pack;TNVBP
;686;;塔可努的原生优化整合包;TNF-Vanilla / 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
;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
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,7 +806,6 @@ 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;;
@@ -866,7 +865,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
tensioncraft;883;;剑拔弩张之时;;TC
;883;;剑拔弩张之时;;TC
project-architect-2;884;;Project Architect 2;;
;885;;接入铁路网络:我的世界铁路;MTR Railway Network Access;RNA-MTR
create-mekanized;886;;Create: Mekanized;;
@@ -923,24 +922,23 @@ 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;
simple-tfc;940;;简单群峦;Simple TFC;SPTFC
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;SJ
;948;;希望与绝望-惊变100天;Hope and Despair;had
;947;;星地征程;Starland Journey re;SJRE
;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;;Hungry For Tech 2;;
hungry-for-tech-2;955;;渴望科技2;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;
@@ -985,7 +983,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
journey-of-reincarnation;1004;;转世之旅;Tensura: 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;;
@@ -1002,7 +1000,7 @@ kumo-o-tagayasu;1017;;耕云钓月;Kumo O Tagayasu;KOTS
;1018;;无限战争4;Infinity War4;INF4
;1019;;群峦传说:工业长路;;
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
asig;1021;;浅忆基础整合;Asai Integrated Modpack;
create-star-valley;1022;;Create: Gears of History;;
;1023;;阿尔法计划;AlphaPlan;AP
spell-dimension;1024;;咒次元;Spell Dimension;SD
@@ -1015,7 +1013,7 @@ terminal;1030;;终末;Terminal;
isolated-crystal-3;1031;;异次元水晶 3;Isolated Crystal 3;
;1032;;机械动力:无限总是小于零;Create:Infinity Always Smaller Than Zero;CIASZ
;1033;;踪灭I-起源;;
;1034;;踪灭II;;
;1034;;踪灭II;Endtime 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
@@ -1075,7 +1073,7 @@ roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
egged-up;1094;;Egged Up;;
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;VEFC
;1096;;愚者;The Fool;TF
the-fool;1096;;愚者;The Fool;TF
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
@@ -1119,11 +1117,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;;剑与王国;;SNK
;1140;;剑与王国;;sword_and_kingdom
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 - QUESTS NOW!;;
create-ultimate-selection;1144;;Create: Ultimate Selection;;
;1145;;元素觉醒;Elemental Awakening;
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
;1147;;锻造之旅;;
@@ -1145,13 +1143,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
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
;1167;;极致沉浸;Immersive Ultra;
;1168;;Polymania!;;
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
beloong;1170;;化龍;BeLoong;
;1171;;畅游自然;;SiF
;1171;;畅游自然;;SiN
;1172;;退休工艺;Retirement Craft;RmC
;1173;;溯光暗潮:云影;NS's Random Gunfight;NRG
;1173;;溯光暗潮;TRACING LIGHT · DARK TIDE;NRG
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
;1175;;硬核惊变;Hardcore Surprise;HS
society-sunlit-valley;1176;;Society: Sunlit Valley;;
@@ -1179,7 +1177,7 @@ repalmon;1198;;Re帕鲁梦;RePalmon;RPLM
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
arcaneaxis-ars;1200;;秘法象限;ArcaneAxis;ARS
gregtech-easy;1201;;格雷科技简单版;GregTech Easy;GTE
;1202;;机境交响;Creation Exploring the Road of;CETR
create-exploring-the-path-of;1202;;机境交响;Creation Exploring the Road of;CETR
multiblockful-skies;1203;;Multiblockful Skies;;
;1204;;刀剑物语;SwordStory;SS
;1205;;无垠征途;The Unbounded Expedition;TUE
@@ -1194,13 +1192,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;;
;1223;;重度机械症;Mechanomania;
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
@@ -1221,17 +1219,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;;FTB StoneBlock 4;;SB4
ftb-stoneblock-4;1244;;石头世界4;FTB StoneBlock 4;SB4
techevdiscovery;1245;;TechEv || Discovery;;
g3-project;1246;;G3 project;;
;1247;;新州;Xin Zhou;XZ
;1247;;新州;Xinzhou;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 Architecture Dreams;TAD
;1252;;闲居物语;Travel Serere Homestead;TSH
high-altitude-alchemy;1253;;High-Altitude Alchemy;;
;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
create-boundary-engineer;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
skyopolis-evolved;1255;;Skyopolis: Evolved;;
;1256;;IFR的1.12.2优化整合包;IFR's Forgified Optimization;IFO
;1257;;无穷匠艺;;
@@ -1262,7 +1260,7 @@ mythcrafts;1281;;MYTHCRAFT 5;;MC
;1282;;牧场物语;Harvest Moon;
;1283;;逃逸:寄生末日;SRP: Escape Parasite Doom;SRP:epd
;1284;;Ice and M;;IAM
;1285;;秋水;autumn floods;AF
autumn-floods;1285;;秋水;autumn floods;AF
;1286;;V2O5;;
;1287;;地牢冒险者;Dungeon Adventurer;DA
ultramodern-optimization;1289;;超现代优化;Ultramodern optimization;UO
@@ -1273,14 +1271,14 @@ pandemonium-nexus-anarchic-aeon;1293;;万象枢纽:无序纪元;Pandemonium Ne
;1294;;菌染亵渎;Fungal Infection Blasphemy;FIB
;1295;;Ice and M:Sky;;IAMS
;1296;;TUFF PERFORMANCE;;
;1297;;灾厄巫咒;;
;1297;;灾厄巫咒;Curlamoety;
;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:New Horizons;SMNH
;1304;;艾斯爱慕:无限构件:现代工业;Spore and M:Infinity Mechanism:Modern Industrialization;SMIM
otherworld-dnd;1305;;Otherworld [Dungeons & Dragons];;
;1306;;奇幻起源;Fantasy Origins;
projects;1307;;ProjectS;;
@@ -1318,7 +1316,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
;1342;;机械动力:探险生存 2;Create Adventure Survival 2;
create-adventure-survival-ii;1342;;机械动力:探险生存 2;Create Adventure Survival II;
;1343;;云游四海;WAYFARING;WF
;1344;;阿卡迪亚的天启;The Apocalypse of Arcadia;TAOA
;1345;;纯净原版・焕新;Pure Vanilla Renewal;PVR
@@ -1327,4 +1325,110 @@ day-of-mind;1341;;DayOfMind;;DOM
dragns-o-suite-pack;1348;;DragN's O-Suite Pack;;
;1349;;机械动力:工业与牧歌;Create: Industry and Pastoral;CIAP
;1350;;灾厄纪行;;
beebeeblock-the-skyhive;1351;;蜂蜂空坊;Beebeeblock: The Skyhive;BTS
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
File diff suppressed because it is too large. Load diff
@@ -1,82 +1,119 @@
{
"$schema" : "https://schemas.glavo.site/hmcl/theme-pack/1.0.0",
"id" : "hmcl.classic",
"version" : "1.0.0",
"name" : {
"default" : "Classic",
"zh-Hans" : "经典主题",
"zh-Hant" : "經典主題"
"$schema": "https://schemas.glavo.site/hmcl/theme-pack/1.0.0",
"id": "hmcl.classic",
"version": "1.0.0",
"name": {
"default": "Classic",
"es": "Clásico",
"ja": "クラシック",
"ru": "Классическая",
"uk": "Класична",
"zh-Hans": "经典主题",
"zh-Hant": "經典主題",
"lzh": "舊觀"
},
"description" : {
"default" : "Classic HMCL launcher themes",
"zh-Hans" : "HMCL 启动器经典主题",
"zh-Hant" : "HMCL 啟動器經典主題"
"description": {
"default": "Classic HMCL launcher themes",
"es": "Temas clásicos del launcher HMCL",
"ja": "HMCL ランチャーのクラシックテーマ",
"ru": "Классические темы лаунчера HMCL",
"uk": "Класичні теми лаунчера HMCL",
"zh-Hans": "HMCL 启动器经典主题",
"zh-Hant": "HMCL 啟動器經典主題",
"lzh": "HMCL 啟動器之舊觀"
},
"authors" : [
"authors": [
{
"name" : "HMCL"
"name": {
"default": "HMCL",
"en-Qabs": "ꞀƆWH"
}
}
],
"themes" : [
"themes": [
{
"id" : "2021-08-26",
"name" : "HMCL 3.15",
"description" : {
"default" : "HMCL 3.15 classic appearance",
"zh-Hans" : "HMCL 3.15 经典外观",
"zh-Hant" : "HMCL 3.15 經典外觀"
"id": "2021-08-26",
"name": {
"default": "Current Theme",
"es": "Tema actual",
"ja": "現在のテーマ",
"ru": "Текущая тема",
"uk": "Поточна тема",
"zh-Hans": "当前主题",
"zh-Hant": "當前主題",
"lzh": "今觀"
},
"color" : "#5C6BC0",
"brightness" : "light",
"colorStyle" : "fidelity",
"background" : {
"type" : "builtin",
"id" : "2021-08-26",
"opacity" : 1.0
"description": {
"default": "HMCL current theme",
"es": "Tema actual de HMCL",
"ja": "HMCL の現在のテーマ",
"ru": "Текущая тема HMCL",
"uk": "Поточна тема HMCL",
"zh-Hans": "HMCL 当前主题",
"zh-Hant": "HMCL 當前主題",
"lzh": "HMCL 之今觀"
},
"titleBar" : {
"transparent" : false
"color": "#5C6BC0",
"brightness": "light",
"colorStyle": "fidelity",
"background": {
"type": "builtin",
"id": "2021-08-26",
"opacity": 1.0
},
"titleBar": {
"transparent": false
}
},
{
"id" : "2016-02-25",
"name" : "HMCL 3.3",
"description" : {
"default" : "HMCL 3.3 classic appearance",
"zh-Hans" : "HMCL 3.3 经典外观",
"zh-Hant" : "HMCL 3.3 經典外觀"
"id": "2016-02-25",
"name": "HMCL 3.3",
"description": {
"default": "HMCL 3.3 classic appearance",
"es": "Apariencia clásica de HMCL 3.3",
"ja": "HMCL 3.3 のクラシックな外観",
"ru": "Классический вид HMCL 3.3",
"uk": "Класичний вигляд HMCL 3.3",
"zh-Hans": "HMCL 3.3 经典外观",
"zh-Hant": "HMCL 3.3 經典外觀",
"lzh": "HMCL 3.3 之舊觀"
},
"color" : "#5C6BC0",
"brightness" : "light",
"colorStyle" : "fidelity",
"background" : {
"type" : "builtin",
"id" : "2016-02-25",
"opacity" : 1.0
"color": "#5C6BC0",
"brightness": "light",
"colorStyle": "fidelity",
"background": {
"type": "builtin",
"id": "2016-02-25",
"opacity": 1.0
},
"titleBar" : {
"transparent" : true
"titleBar": {
"transparent": true
}
},
{
"id" : "2015-06-22",
"name" : {
"default" : "HMCL 2.3"
"id": "2015-06-22",
"name": {
"default": "HMCL 2.3"
},
"description" : {
"default" : "HMCL 2.3 classic appearance",
"zh-Hans" : "HMCL 2.3 经典外观",
"zh-Hant" : "HMCL 2.3 經典外觀"
"description": {
"default": "HMCL 2.3 classic appearance",
"es": "Apariencia clásica de HMCL 2.3",
"ja": "HMCL 2.3 のクラシックな外観",
"ru": "Классический вид HMCL 2.3",
"uk": "Класичний вигляд HMCL 2.3",
"zh-Hans": "HMCL 2.3 经典外观",
"zh-Hant": "HMCL 2.3 經典外觀",
"lzh": "HMCL 2.3 之舊觀"
},
"color" : "#E67E22",
"brightness" : "light",
"colorStyle" : "fidelity",
"background" : {
"type" : "builtin",
"id" : "2015-06-22"
"color": "#E67E22",
"brightness": "light",
"colorStyle": "fidelity",
"background": {
"type": "builtin",
"id": "2015-06-22"
},
"titleBar" : {
"transparent" : false
"titleBar": {
"transparent": false
}
}
]
@@ -4,17 +4,31 @@
"version": "1.0.0",
"name": {
"default": "Default",
"en-Qabs": "ʇꞁnɐɟǝᗡ",
"es": "Predeterminado",
"ja": "デフォルト",
"ru": "По умолчанию",
"uk": "За замовчуванням",
"zh-Hans": "默认主题",
"zh-Hant": "預設主題"
"zh-Hant": "預設主題",
"lzh": "預設主題"
},
"description": {
"default": "HMCL Default",
"es": "HMCL Predeterminado",
"ja": "HMCL デフォルト",
"ru": "HMCL По умолчанию",
"uk": "HMCL За замовчуванням",
"zh-Hans": "HMCL 默认主题",
"zh-Hant": "HMCL 預設主題"
"zh-Hant": "HMCL 預設主題",
"lzh": "HMCL 預設主題"
},
"authors": [
{
"name": "HMCL"
"name": {
"default": "HMCL",
"en-Qabs": "ꞀƆWH"
}
}
],
"theme": {}
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.lang.reflect.Field;
@@ -95,8 +96,8 @@ public final class GameDirectoriesTest {
GameDirectories gameDirectories = Objects.requireNonNull(LegacyConfigMigrator.extractGameDirectoriesFromConfigJson(settings));
GameDirectoryID defaultGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Default");
GameDirectoryID homeGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Home");
GameDirectoryID defaultGameDirectoryId = GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID;
GameDirectoryID homeGameDirectoryId = GameDirectoryManager.HOME_GAME_DIRECTORY_ID;
GameDirectory defaultGameDirectory = gameDirectories.getGameDirectories().stream()
.filter(gameDirectory -> defaultGameDirectoryId.equals(gameDirectory.getId()))
.findFirst()
@@ -356,12 +357,36 @@ public final class GameDirectoriesTest {
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
userDirectories,
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
assertEquals(GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID, localGameDirectory.getId());
assertEquals(GameDirectoryManager.HOME_GAME_DIRECTORY_ID, userGameDirectory.getId());
assertEquals(2, GameDirectoryManager.getGameDirectories().size());
assertTrue(GameDirectoryManager.getGameDirectories().contains(localGameDirectory));
assertTrue(GameDirectoryManager.getGameDirectories().contains(userGameDirectory));
}
}
/// Tests that newly created stores use the stable IDs for their built-in game directories.
@Test
public void createsBuiltInGameDirectoriesWithStableIdsForNewStores() throws ReflectiveOperationException {
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
userDirectories.setNewlyCreated(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.setUserFile(false);
localDirectories.setNewlyCreated(true);
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
GameDirectory localGameDirectory = assertSingleDefaultGameDirectory(localDirectories, PortablePath.of(".minecraft"));
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
userDirectories,
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
assertEquals(GameDirectoryManager.DEFAULT_GAME_DIRECTORY_ID, localGameDirectory.getId());
assertEquals(GameDirectoryManager.HOME_GAME_DIRECTORY_ID, userGameDirectory.getId());
}
}
/// Tests that the selected game directory getter restores a valid selection instead of returning null.
@Test
public void selectedGameDirectoryGetterNeverReturnsNullWhenGameDirectoriesAreLoaded() throws ReflectiveOperationException {
@@ -407,6 +432,231 @@ public final class GameDirectoriesTest {
}
}
/// Tests that new isolated installing instances resolve content directories under the version root before metadata is saved.
@Test
public void newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists(@TempDir Path tempDirectory)
throws ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002");
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.MODDED);
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(defaultPreset);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()));
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
String id = "1.21.11-fabric";
assertFalse(repository.hasVersion(id));
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
repository.applyDefaultIsolationSettingForNewInstance(id, true);
assertEquals(repository.getVersionRoot(id), repository.getRunDirectory(id));
assertEquals(repository.getVersionRoot(id).resolve("mods"), repository.getModsDirectory(id));
assertTrue(repository.removeVersionFromDisk(id));
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
}
}
/// Tests that instance settings without an explicit parent use the default preset.
@Test
public void nullInstanceParentUsesDefaultPresetInsteadOfLegacyGameDirectoryPreset()
throws ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
GameSettingsPresetID legacyPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(defaultPreset, legacyPreset);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of("local/Dev"),
legacyPresetId);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
assertSame(defaultPreset, repository.getParentGameSettings(new GameSettings.Instance()));
}
}
/// Tests that legacy per-version settings migration stores the game directory preset as an explicit parent.
@Test
public void legacyInstanceSettingsMigrationStoresLegacyGameDirectoryPresetAsParent(@TempDir Path tempDirectory)
throws IOException, ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
GameSettingsPresetID legacyPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(
new GameSettings.Preset(defaultPresetId),
new GameSettings.Preset(legacyPresetId));
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()),
legacyPresetId);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
Path versionRoot = repository.getVersionRoot("1.20.1");
Files.createDirectories(versionRoot);
Files.writeString(versionRoot.resolve("hmclversion.cfg"), """
{
"usesGlobal": true
}
""");
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
assertEquals(legacyPresetId, setting.parentProperty().getValue());
}
}
/// Tests that startup migration leaves legacy per-version settings for lazy migration.
@Test
public void startupMigrationSkipsLegacyInstanceSettingsFile(@TempDir Path tempDirectory)
throws IOException, ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
GameSettingsPresetID legacyPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(
new GameSettings.Preset(defaultPresetId),
new GameSettings.Preset(legacyPresetId));
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()),
legacyPresetId);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
writeVersionJson(repository, "1.20.1");
Path versionRoot = repository.getVersionRoot("1.20.1");
Files.writeString(versionRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME), """
{
"usesGlobal": true
}
""");
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
assertFalse(Files.exists(repository.getInstanceConfigDirectory("1.20.1")
.resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME)));
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
assertEquals(legacyPresetId, setting.parentProperty().getValue());
}
}
/// Tests that existing versions without instance settings store the legacy game directory parent.
@Test
public void absentInstanceSettingsStoreLegacyGameDirectoryPresetAsParent(@TempDir Path tempDirectory)
throws IOException, ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
GameSettingsPresetID legacyPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
legacyPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(
new GameSettings.Preset(defaultPresetId),
legacyPreset);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()),
legacyPresetId);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
writeVersionJson(repository, "1.20.1");
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
assertEquals(legacyPresetId, setting.parentProperty().getValue());
assertTrue(setting.getOverrideProperties().contains(GameSettings.PROPERTY_RUNNING_DIRECTORY));
}
}
/// Tests that versions created after migration do not inherit the legacy game directory parent.
@Test
public void newInstanceAfterMigrationDoesNotUseLegacyGameDirectoryParent(@TempDir Path tempDirectory)
throws IOException, ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
GameSettingsPresetID legacyPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.NEVER);
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
legacyPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(defaultPreset, legacyPreset);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()),
legacyPresetId);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
writeVersionJson(repository, "1.20.1");
assertNull(repository.getInstanceGameSettings("1.20.1"));
}
}
/// Temporary static state override for game directory tests.
private static final class GameDirectoryEnvironment implements AutoCloseable {
/// The reflected SettingsManager local game directories field.
@@ -418,6 +668,9 @@ public final class GameDirectoriesTest {
/// The reflected SettingsManager launcher settings field.
private final Field launcherSettingsField;
/// The reflected SettingsManager game settings presets field.
private final Field gameSettingsPresetsField;
/// The reflected SettingsManager local game directories access field.
private final Field localGameDirectoriesAccessField;
@@ -448,6 +701,9 @@ public final class GameDirectoriesTest {
/// The previous launcher settings instance.
private final Object previousLauncherSettings;
/// The previous game settings presets instance.
private final Object previousGameSettingsPresets;
/// The previous local game directories access.
private final SettingFileAccess previousLocalGameDirectoriesAccess;
@@ -469,12 +725,22 @@ public final class GameDirectoriesTest {
/// The previous repository map entries.
private final Map<GameDirectory, HMCLGameRepository> previousRepositories;
/// Replaces game-directory-related static state with the given stores.
/// Replaces game-directory-related static state with the given stores and an empty preset store.
private GameDirectoryEnvironment(GameDirectories localDirectories, GameDirectories userDirectories)
throws ReflectiveOperationException {
this(localDirectories, userDirectories, new GameSettingsPresets());
}
/// Replaces game-directory-related static state with the given stores.
private GameDirectoryEnvironment(
GameDirectories localDirectories,
GameDirectories userDirectories,
GameSettingsPresets gameSettingsPresets)
throws ReflectiveOperationException {
localGameDirectoriesField = SettingsManager.class.getDeclaredField("localGameDirectories");
userGameDirectoriesField = SettingsManager.class.getDeclaredField("userGameDirectories");
launcherSettingsField = SettingsManager.class.getDeclaredField("launcherSettings");
gameSettingsPresetsField = SettingsManager.class.getDeclaredField("gameSettingsPresets");
localGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("localGameDirectoriesAccess");
userGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("userGameDirectoriesAccess");
initializedField = GameDirectoryManager.class.getDeclaredField("initialized");
@@ -485,6 +751,7 @@ public final class GameDirectoriesTest {
localGameDirectoriesField.setAccessible(true);
userGameDirectoriesField.setAccessible(true);
launcherSettingsField.setAccessible(true);
gameSettingsPresetsField.setAccessible(true);
localGameDirectoriesAccessField.setAccessible(true);
userGameDirectoriesAccessField.setAccessible(true);
initializedField.setAccessible(true);
@@ -496,6 +763,7 @@ public final class GameDirectoriesTest {
previousLocalGameDirectories = localGameDirectoriesField.get(null);
previousUserGameDirectories = userGameDirectoriesField.get(null);
previousLauncherSettings = launcherSettingsField.get(null);
previousGameSettingsPresets = gameSettingsPresetsField.get(null);
previousLocalGameDirectoriesAccess = (SettingFileAccess) localGameDirectoriesAccessField.get(null);
previousUserGameDirectoriesAccess = (SettingFileAccess) userGameDirectoriesAccessField.get(null);
previousInitialized = initializedField.getBoolean(null);
@@ -527,6 +795,7 @@ public final class GameDirectoriesTest {
localGameDirectoriesField.set(null, localDirectories);
userGameDirectoriesField.set(null, userDirectories);
launcherSettingsField.set(null, new LauncherSettings());
gameSettingsPresetsField.set(null, gameSettingsPresets);
localGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
userGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
initializedField.setBoolean(null, false);
@@ -558,12 +827,24 @@ public final class GameDirectoriesTest {
localGameDirectoriesField.set(null, previousLocalGameDirectories);
userGameDirectoriesField.set(null, previousUserGameDirectories);
launcherSettingsField.set(null, previousLauncherSettings);
gameSettingsPresetsField.set(null, previousGameSettingsPresets);
localGameDirectoriesAccessField.set(null, previousLocalGameDirectoriesAccess);
userGameDirectoriesAccessField.set(null, previousUserGameDirectoriesAccess);
initializedField.setBoolean(null, previousInitialized);
}
}
/// Writes a minimal valid version json for repository refresh tests.
private static void writeVersionJson(HMCLGameRepository repository, String id) throws IOException {
Path versionRoot = repository.getVersionRoot(id);
Files.createDirectories(versionRoot);
Files.writeString(versionRoot.resolve(id + ".json"), """
{
"id": "%s"
}
""".formatted(id));
}
/// Returns the only default game directory in the given store, asserting its path.
private static GameDirectory assertSingleDefaultGameDirectory(GameDirectories gameDirectories, PortablePath path) {
assertEquals(1, gameDirectories.getGameDirectories().size());
@@ -137,6 +137,7 @@ public final class LauncherSettingsMigrationTest {
assertEquals(ThemeColor.of("#336699"), launcherSettings.customThemeColorProperty().get());
assertEquals(ThemeColorType.CUSTOM, launcherSettings.themeColorTypeProperty().get());
assertTrue(launcherSettings.titleBarTransparentProperty().get());
assertTrue(launcherSettings.windowTransparentProperty().get());
assertEquals("/pictures/background.png", launcherSettings.customBackgroundImagePathProperty().get());
assertEquals("https://example.com/background.png",
launcherSettings.networkBackgroundImageUrlProperty().get());
@@ -148,6 +149,8 @@ public final class LauncherSettingsMigrationTest {
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_COLOR));
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
LauncherSettings.THEME_APPEARANCE_TITLE_BAR_TRANSPARENT));
assertTrue(launcherSettings.getThemeAppearanceOverrides().contains(
LauncherSettings.THEME_APPEARANCE_WINDOW_TRANSPARENT));
assertFalse(launcherSettings.getThemeAppearanceOverrides().contains("networkBackgroundImageCachePolicy"));
assertEquals("/pictures/background.png", serialized.get("customBackgroundImagePath").getAsString());
assertEquals("https://example.com/background.png",
@@ -158,6 +161,7 @@ public final class LauncherSettingsMigrationTest {
assertEquals(ThemeColorType.CUSTOM.name(), serialized.get("themeColorType").getAsString());
assertEquals("dark", serialized.get("themeBrightnessMode").getAsString());
assertTrue(serialized.get("titleBarTransparent").getAsBoolean());
assertTrue(serialized.get("windowTransparent").getAsBoolean());
assertEquals("#336699", serialized.get("customBackgroundPaint").getAsString());
}
}
@@ -0,0 +1,54 @@
/*
* 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.theme;
import javafx.geometry.Insets;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
/// Tests launcher background opacity storage.
@NotNullByDefault
public final class LauncherBackgroundTest {
/// Verifies opacity changes reuse the unmodified JavaFX background.
@Test
public void changesOpacityWithoutChangingBackground() {
Background background = new Background(
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY));
LauncherBackground changed = new LauncherBackground(background, 0.4);
assertSame(background, changed.background());
assertEquals(0.4, changed.opacity());
assertEquals(1.0, ((Color) changed.background().getFills().get(0).getFill()).getOpacity());
}
/// Verifies invalid node opacity values are rejected.
@Test
public void rejectsInvalidOpacity() {
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, -0.1));
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, 1.1));
assertThrows(IllegalArgumentException.class, () -> new LauncherBackground(Background.EMPTY, Double.NaN));
}
}
@@ -87,7 +87,7 @@ public final class ThemePackManagerTest {
launcherSettingsField.set(null, launcherSettings);
ThemePackManager.ExportedThemePack exported =
ThemePackManager.createCurrent("example.export", "Example", "Tester");
ThemePackManager.createCurrent("example.export", "1.0", "Example", "Tester");
Theme theme = exported.manifest().themes().get(0);
ThemeBackgroundSettings background = Objects.requireNonNull(theme.appearance().background());
ThemeBackground.Image image = assertInstanceOf(ThemeBackground.Image.class, background.source());
@@ -146,7 +146,7 @@ public final class ThemePackManagerTest {
launcherSettingsField.set(null, launcherSettings);
ThemePackManager.ExportedThemePack exported =
ThemePackManager.createCurrent("example.export", "Example", "Tester");
ThemePackManager.createCurrent("example.export", "1.0", "Example", "Tester");
try {
Theme theme = exported.manifest().themes().get(0);
ThemeBackgroundSettings background = Objects.requireNonNull(theme.appearance().background());
@@ -86,6 +86,7 @@ public final class ThemePackManifestTest {
assertEquals(BackgroundType.DEFAULT, background.type());
assertEquals(1.0, background.opacity(), 0.0);
assertNull(theme.appearance().windowTransparent());
}
}
@@ -17,17 +17,26 @@
*/
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);
@@ -37,6 +46,15 @@ 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")));
@@ -47,6 +65,7 @@ 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")));
@@ -57,4 +76,66 @@ 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'/>"
)));
}
}
@@ -30,6 +30,7 @@ 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.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
@@ -103,12 +104,13 @@ public final class ForgeNewModMetadata {
@JsonAdapter(AuthorDeserializer.class)
private final String authors;
private final String description;
private final String logoFile;
public Mod() {
this("", "", "", "", "", "", "");
this("", "", "", "", "", "", "", "");
}
public Mod(String modId, String version, String displayName, String side, String displayURL, String authors, String description) {
public Mod(String modId, String version, String displayName, String side, String displayURL, String authors, String description, String logoFile) {
this.modId = modId;
this.version = version;
this.displayName = displayName;
@@ -116,6 +118,7 @@ public final class ForgeNewModMetadata {
this.displayURL = displayURL;
this.authors = authors;
this.description = description;
this.logoFile = logoFile;
}
public String getModId() {
@@ -146,6 +149,10 @@ public final class ForgeNewModMetadata {
return description;
}
public String getLogoFile() {
return logoFile;
}
static final class AuthorDeserializer implements JsonDeserializer<String> {
@Override
public String deserialize(JsonElement authors, Type type, JsonDeserializationContext context) throws JsonParseException {
@@ -235,10 +242,12 @@ public final class ForgeNewModMetadata {
ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);
String logoPath = StringUtils.isNotBlank(mod.getLogoFile()) ? mod.getLogoFile() : metadata.getLogoFile();
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalAddonFile.Description(mod.getDescription()),
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
mod.getDisplayURL(),
metadata.getLogoFile());
logoPath);
}
private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException {
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff. Show more