Compare commits

...
52 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
97 changed files with 12792 additions and 1677 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);
@@ -382,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 -> {
}
}
@@ -520,31 +534,35 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
/// 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;
};
}
public void applyDefaultIsolationSetting(String id) {
if (!hasVersion(id)) {
/// 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 instanceSetting = getInstanceGameSettings(id);
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
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 = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id);
if (setting != null) {
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
}
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 Optional<Path> getVersionIconFile(String id) {
@@ -684,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,
@@ -694,26 +712,31 @@ 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)
.setJava(javaVersion)
.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().available(),
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('=');
@@ -731,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(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);
@@ -838,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() {
@@ -544,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 {
@@ -581,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"));
@@ -628,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"));
}
@@ -640,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));
}
@@ -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));
}
});
}
@@ -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;
}
@@ -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.
@@ -81,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";
@@ -261,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);
@@ -1359,10 +1357,10 @@ public final class LegacyConfigMigrator {
/// 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);
@@ -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);
@@ -660,7 +660,8 @@ public final class ThemePackManager {
currentColorStyle(),
null,
background,
new ThemeTitleBar(currentTitleBarTransparent()));
new ThemeTitleBar(currentTitleBarTransparent()),
currentWindowTransparent());
Theme theme = new Theme(
null,
null,
@@ -711,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;
@@ -755,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
@@ -795,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;
@@ -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);
@@ -121,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"));
@@ -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;
}
@@ -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,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");
}
@@ -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);
}
}
}
@@ -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,11 +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.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
@@ -71,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;
@@ -107,6 +105,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
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;
@@ -133,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);
@@ -158,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")),
@@ -191,6 +196,7 @@ 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)
: i18n("settings.type.global.preset.default"));
@@ -274,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) {
@@ -362,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);
@@ -381,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();
@@ -418,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);
});
@@ -428,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(
@@ -837,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);
@@ -865,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;
}
@@ -880,6 +892,49 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
: 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 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();
}
/// 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.
private void bindJavaInheritanceButton(ComponentSublist sublist) {
if (isPresetSetting) {
@@ -1033,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.
@@ -1050,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.
@@ -1093,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) {
@@ -1178,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);
}
@@ -1416,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;
@@ -1434,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();
@@ -1499,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);
@@ -1524,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;
@@ -1542,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();
@@ -1607,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);
@@ -1660,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) -> {
@@ -1728,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();
@@ -1747,6 +1810,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
event.consume();
});
@@ -1766,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) -> {
@@ -1779,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);
@@ -1825,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();
@@ -1841,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;
@@ -1857,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();
@@ -1921,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);
@@ -1937,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();
@@ -1947,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;
@@ -1963,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();
@@ -2022,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());
@@ -2057,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) {
@@ -2069,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,
@@ -2093,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");
@@ -2139,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;
}
@@ -2160,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();
@@ -2170,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;
@@ -2186,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();
@@ -2251,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));
@@ -2268,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;
@@ -2280,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 {
@@ -2288,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();
@@ -2338,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));
@@ -2409,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();
}
@@ -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().available(),
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.total())));
allocatedMemoryLabel.setText(i18n(
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.available()
? (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.available(),
autoMemoryEnabled)),
GIGABYTES.convertFromBytes(memoryStatus.available())));
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);
@@ -40,6 +40,7 @@ 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;
@@ -50,13 +51,16 @@ 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
@@ -81,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) -> {
@@ -198,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'};
@@ -209,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 = {
@@ -249,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.
@@ -436,7 +700,7 @@ public final class ImageUtils {
}
}
PngAnimationControl animationControl = sequence.getAnimationControl();
@Nullable PngAnimationControl animationControl = sequence.getAnimationControl();
int cycleCount;
if (animationControl != null) {
cycleCount = animationControl.numPlays();
@@ -465,6 +729,7 @@ public final class ImageUtils {
return argb;
}
/// Prevents instantiation.
private ImageUtils() {
}
}
@@ -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());
@@ -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();
@@ -323,10 +379,11 @@ public class PersonalizationPage extends StackPane {
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));
@@ -443,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) {
@@ -459,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);
@@ -1076,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,
@@ -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;
@@ -522,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) {
@@ -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;
}
+33 -21
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 {
@@ -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\
@@ -1535,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
@@ -1547,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
@@ -1604,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
@@ -1632,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.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
@@ -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
@@ -1293,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
@@ -1326,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=主題模式
@@ -1338,8 +1339,9 @@ 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=自動選取下載來源
@@ -1395,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=手動選取記憶體
@@ -1423,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.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
@@ -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如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -1298,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
@@ -1331,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=主题模式
@@ -1343,8 +1344,9 @@ 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=自动选择下载源
@@ -1400,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=手动选择内存
@@ -1428,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.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
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": {}
@@ -96,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()
@@ -357,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 {
@@ -408,6 +432,44 @@ 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()
@@ -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));
}
}
@@ -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'/>"
)));
}
}
@@ -38,7 +38,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModManager extends LocalAddonManager<LocalModFile> {
public static final List<String> MOD_EXTENSIONS = List.of("jar", "litemod");
public static final List<String> MOD_EXTENSIONS = List.of("jar", "zip", "litemod");
@FunctionalInterface
private interface ModMetadataReader {
@@ -58,6 +58,7 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
);
map.put("jar", zipReaders);
map.put("zip", zipReaders);
map.put("litemod", List.of(pair(LiteModMetadata::fromFile, ModLoaderType.LITE_LOADER)));
READERS = map;
@@ -32,17 +32,19 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Checksum;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -184,23 +186,65 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
}
}
@Override
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream stream = Files.newInputStream(file)) {
byte[] buf = new byte[1024];
int len;
while ((len = stream.read(buf, 0, buf.length)) != -1) {
/// Calculates the CurseForge fingerprint without retaining the filtered file in memory.
static long calculateFingerprint(Path file) throws IOException {
try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
long startPosition = channel.position();
byte[] bufferArray = new byte[1024 * 1024];
ByteBuffer buffer = ByteBuffer.wrap(bufferArray);
long filteredLength = 0;
while (channel.read(buffer) > 0) {
int len = buffer.position();
for (int i = 0; i < len; i++) {
byte b = buf[i];
byte b = bufferArray[i];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
baos.write(b);
filteredLength++;
}
}
buffer.clear();
}
}
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
channel.position(startPosition);
Checksum hasher = MurmurHash2.hash32(filteredLength, 1);
while (channel.read(buffer) > 0) {
int len = buffer.position();
int pos = 0;
while (pos < len) {
byte b = bufferArray[pos];
if (b == 0x9 || b == 0xa || b == 0xd || b == 0x20) {
break;
}
pos++;
}
if (pos < len) {
int pos2 = pos + 1;
while (pos2 < len) {
byte b = bufferArray[pos2];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
bufferArray[pos++] = b;
}
pos2++;
}
}
hasher.update(bufferArray, 0, pos);
buffer.clear();
}
return hasher.getValue();
} catch (IllegalArgumentException | IllegalStateException e) {
throw new IOException(e);
}
}
/// Finds the remote CurseForge version matching a local file.
@Override
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
long hash = calculateFingerprint(file);
if (hash == 811513880) { // Workaround for https://github.com/HMCL-dev/HMCL/issues/4597
return Optional.empty();
}
@@ -21,15 +21,17 @@ package org.jackhuang.hmcl.download.java;
* @author Glavo
*/
public enum JavaPackageType {
JRE(false, false),
JDK(true, false),
JREFX(false, true),
JDKFX(true, true);
JRE("JRE", false, false),
JDK("JDK", true, false),
JREFX("JRE FX", false, true),
JDKFX("JDK FX", true, true);
private final String displayName;
private final boolean jdk;
private final boolean javafx;
JavaPackageType(boolean jdk, boolean javafx) {
JavaPackageType(String displayName, boolean jdk, boolean javafx) {
this.displayName = displayName;
this.jdk = jdk;
this.javafx = javafx;
}
@@ -41,6 +43,10 @@ public enum JavaPackageType {
return javafx ? JREFX : JRE;
}
public String getDisplayName() {
return displayName;
}
public boolean isJDK() {
return jdk;
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -85,12 +86,17 @@ public final class QuiltInstallTask extends Task<Version> {
@Override
public void execute() {
setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), QuiltInfo.class), remote.getGameVersion(), remote.getSelfVersion()));
setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), QuiltInfo.class), remote.getSelfVersion()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true));
}
private Version getPatch(QuiltInfo quiltInfo, String gameVersion, String loaderVersion) {
/// Creates the Quilt version patch described by the metadata response.
///
/// @param quiltInfo the metadata returned by Quilt Meta
/// @param loaderVersion the Quilt Loader version
/// @return the version patch to install
static Version getPatch(QuiltInfo quiltInfo, String loaderVersion) {
JsonObject launcherMeta = quiltInfo.launcherMeta;
Arguments arguments = new Arguments();
@@ -118,7 +124,9 @@ public final class QuiltInstallTask extends Task<Version> {
}
// libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.hashed.maven), getMavenRepositoryByGroup(quiltInfo.hashed.maven), null));
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null));
if (quiltInfo.intermediary != null) {
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null));
}
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.loader.maven), getMavenRepositoryByGroup(quiltInfo.loader.maven), null));
return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries);
@@ -139,11 +147,11 @@ public final class QuiltInstallTask extends Task<Version> {
@JsonSerializable
public static class QuiltInfo {
private final LoaderInfo loader;
private final IntermediaryInfo hashed;
private final IntermediaryInfo intermediary;
private final @Nullable IntermediaryInfo hashed;
private final @Nullable IntermediaryInfo intermediary;
private final JsonObject launcherMeta;
public QuiltInfo(LoaderInfo loader, IntermediaryInfo hashed, IntermediaryInfo intermediary, JsonObject launcherMeta) {
public QuiltInfo(LoaderInfo loader, @Nullable IntermediaryInfo hashed, @Nullable IntermediaryInfo intermediary, JsonObject launcherMeta) {
this.loader = loader;
this.hashed = hashed;
this.intermediary = intermediary;
@@ -154,11 +162,11 @@ public final class QuiltInstallTask extends Task<Version> {
return loader;
}
public IntermediaryInfo getHashed() {
public @Nullable IntermediaryInfo getHashed() {
return hashed;
}
public IntermediaryInfo getIntermediary() {
public @Nullable IntermediaryInfo getIntermediary() {
return intermediary;
}
@@ -233,7 +233,7 @@ public class DefaultGameRepository implements GameRepository {
public boolean removeVersionFromDisk(String id) {
if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY)
return false;
if (!versions.containsKey(id))
if (versions == null || !versions.containsKey(id))
return FileUtils.deleteDirectoryQuietly(getVersionRoot(id));
Path file = getVersionRoot(id);
if (Files.notExists(file))
@@ -17,7 +17,11 @@
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.zip.Checksum;
/**
* Implementation of the MurmurHash2 32-bit and 64-bit hash functions.
@@ -48,6 +52,7 @@ import java.nio.charset.StandardCharsets;
* Original MurmurHash2 c++ code</a>
* @since 1.13
*/
@NotNullByDefault
public final class MurmurHash2 {
// Constants for 32-bit variant
@@ -64,6 +69,177 @@ public final class MurmurHash2 {
private MurmurHash2() {
}
/// Creates a streaming MurmurHash2 32-bit checksum for exactly `length` bytes.
///
/// The length is incorporated into the initial hash state using its low 32 bits. Calls to
/// [Checksum#update(int)] and [Checksum#update(byte[], int, int)] may divide the input at
/// arbitrary byte boundaries. [Checksum#getValue()] returns the hash as an unsigned 32-bit
/// value represented by a `long`, and does not change the checksum state.
///
/// The returned checksum verifies the number of supplied bytes when `getValue()` is called.
/// If too few bytes have been supplied, the caller may continue updating the checksum and call
/// `getValue()` again. Once too many bytes have been supplied, [Checksum#reset()] must be called
/// before a value can be obtained. Resetting retains the configured length and seed. The
/// returned checksum is mutable and is not safe for concurrent use.
///
/// @param length the exact number of input bytes
/// @param seed the initial seed value
/// @return a new checksum initialized with the specified length and seed
/// @throws IllegalArgumentException if `length` is negative
public static Checksum hash32(final long length, final int seed) {
if (length < 0) {
throw new IllegalArgumentException("length must not be negative: " + length);
}
return new Hash32Checksum(length, seed);
}
/// Computes a MurmurHash2 32-bit value from updates whose total length is known in advance.
private static final class Hash32Checksum implements Checksum {
/// The exact number of bytes required before the hash can be obtained.
private final long expectedLength;
/// The hash state restored by [#reset()].
private final int initialHash;
/// The hash state after all complete four-byte blocks received so far.
private int hash;
/// The number of bytes counted before [#inputLengthExceeded] becomes `true`.
private long inputLength;
/// Whether more than [#expectedLength] bytes have been received.
private boolean inputLengthExceeded;
/// Up to three unprocessed bytes packed in little-endian order.
private int tail;
/// The number of bytes currently stored in [#tail].
private int tailLength;
/// Creates a checksum with a precomputed initial hash state.
///
/// @param expectedLength the exact number of input bytes
/// @param seed the initial seed value
private Hash32Checksum(long expectedLength, int seed) {
this.expectedLength = expectedLength;
this.initialHash = seed ^ (int) expectedLength;
this.hash = initialHash;
}
/// Incorporates the low eight bits of `value` into this checksum.
///
/// @param value the value whose low eight bits are incorporated
@Override
public void update(int value) {
addInputLength(1);
appendByte(value);
}
/// Incorporates `length` bytes beginning at `offset` into this checksum.
///
/// @param data the array containing the input bytes
/// @param offset the offset of the first input byte
/// @param length the number of bytes to incorporate
@Override
public void update(byte[] data, int offset, int length) {
Objects.checkFromIndexSize(offset, length, data.length);
addInputLength(length);
int index = offset;
final int end = offset + length;
while (tailLength != 0 && index < end) {
appendByte(data[index++]);
}
while (index <= end - Integer.BYTES) {
mixBlock(ByteArray.getIntLE(data, index));
index += Integer.BYTES;
}
while (index < end) {
appendByte(data[index++]);
}
}
/// Returns the MurmurHash2 value after verifying the exact input length.
///
/// @return the unsigned 32-bit hash value represented by a `long`
/// @throws IllegalStateException if the number of supplied bytes differs from the expected
/// length
@Override
public long getValue() {
if (inputLengthExceeded) {
throw new IllegalStateException(
"Expected " + expectedLength + " bytes, but received more than expected");
}
if (inputLength != expectedLength) {
throw new IllegalStateException(
"Expected " + expectedLength + " bytes, but received " + inputLength);
}
int result = hash;
if (tailLength != 0) {
result ^= tail;
result *= M32;
}
result ^= result >>> 13;
result *= M32;
result ^= result >>> 15;
return Integer.toUnsignedLong(result);
}
/// Restores this checksum to its initial state while retaining its expected length and seed.
@Override
public void reset() {
hash = initialHash;
inputLength = 0;
inputLengthExceeded = false;
tail = 0;
tailLength = 0;
}
/// Records that `length` more input bytes have been supplied.
///
/// @param length the non-negative number of additional bytes
private void addInputLength(int length) {
if (inputLengthExceeded) {
return;
}
if (length > expectedLength - inputLength) {
inputLengthExceeded = true;
} else {
inputLength += length;
}
}
/// Buffers one byte and mixes the resulting block when four bytes are available.
///
/// @param value the value whose low eight bits are appended
private void appendByte(int value) {
tail |= (value & 0xff) << (tailLength * Byte.SIZE);
tailLength++;
if (tailLength == Integer.BYTES) {
mixBlock(tail);
tail = 0;
tailLength = 0;
}
}
/// Mixes one little-endian four-byte block into the current hash state.
///
/// @param block the block to mix
private void mixBlock(int block) {
int mixedBlock = block;
mixedBlock *= M32;
mixedBlock ^= mixedBlock >>> R32;
mixedBlock *= M32;
hash *= M32;
hash ^= mixedBlock;
}
}
/**
* Generates a 32-bit hash from byte array with the given length and seed.
*
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,7 +27,7 @@ import java.util.Map;
/// A wrapper for `Map<String, Object>`, supporting type-safe reading and writing of values.
///
/// @author Glavo
/// @author Glavo
public final class SettingsMap {
public record Key<T>(String key) {
}
@@ -87,6 +89,19 @@ public final class SettingsMap {
map.clear();
}
/// Returns whether the selected installation includes any non-vanilla component.
public boolean isInstallingModdedVersion() {
for (LibraryAnalyzer.LibraryType value : LibraryAnalyzer.LibraryType.values()) {
if (value != LibraryAnalyzer.LibraryType.MINECRAFT
&& value.isModLoader()
&& get(value.getPatchId()) instanceof RemoteVersion) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "SettingsMap" + map;
@@ -137,14 +137,14 @@ public final class CompressingUtils {
}
public static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException {
return new ZipArchiveReader(zipFile, charset);
return new ZipArchiveReader(zipFile, charset, true, true);
}
public static ZipArchiveReader openZipFileWithPossibleEncoding(Path zipFile, Charset possibleEncoding) throws IOException {
if (possibleEncoding == null)
possibleEncoding = StandardCharsets.UTF_8;
ZipArchiveReader zipReader = new ZipArchiveReader(Files.newByteChannel(zipFile));
ZipArchiveReader zipReader = new ZipArchiveReader(zipFile, StandardCharsets.UTF_8, true, true);
Charset suitableEncoding;
try {
@@ -161,7 +161,7 @@ public final class CompressingUtils {
}
zipReader.close();
return new ZipArchiveReader(Files.newByteChannel(zipFile), suitableEncoding);
return new ZipArchiveReader(zipFile, suitableEncoding, true, true);
}
public static final class Builder {
@@ -131,78 +131,105 @@ public abstract class HttpRequest {
return con;
}
public static class HttpGetRequest extends HttpRequest {
protected HttpGetRequest(String url) {
super(url, "GET");
protected void checkResponseCode(HttpURLConnection con) throws IOException {
int code = con.getResponseCode();
if (code / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(code)) {
try {
throw new ResponseCodeException(this.url, code, NetworkUtils.readFullyAsString(con));
} catch (IOException e) {
throw new ResponseCodeException(this.url, code, e);
}
}
}
}
public static abstract class HttpSimpleRequest extends HttpRequest {
protected HttpSimpleRequest(String url, String method) {
super(url, method);
}
@Override
public String getString() throws IOException {
return getStringWithRetry(() -> {
HttpURLConnection con = createConnection();
con = resolveConnection(con);
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(con.getInputStream()) : con.getInputStream());
checkResponseCode(con);
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding())
? IOUtils.wrapFromGZip(con.getInputStream())
: con.getInputStream());
}, retryTimes);
}
}
public static final class HttpPostRequest extends HttpRequest {
private byte[] bytes;
public static class HttpGetRequest extends HttpSimpleRequest {
protected HttpGetRequest(String url) { super(url, "GET"); }
}
private HttpPostRequest(String url) {
super(url, "POST");
public static class HttpDeleteRequest extends HttpSimpleRequest {
protected HttpDeleteRequest(String url) { super(url, "DELETE"); }
}
@SuppressWarnings("unchecked")
public static abstract class HttpEntityRequest<T extends HttpEntityRequest<T>> extends HttpRequest {
protected byte[] bytes;
protected HttpEntityRequest(String url, String method) {
super(url, method);
}
public HttpPostRequest contentType(String contentType) {
public T contentType(String contentType) {
headers.put("Content-Type", contentType);
return this;
return (T) this;
}
public HttpPostRequest json(Object payload) throws JsonParseException {
public T json(Object payload) throws JsonParseException {
return string(payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
}
public HttpPostRequest form(Map<String, String> params) {
public T form(Map<String, String> params) {
return string(NetworkUtils.withQuery("", params), "application/x-www-form-urlencoded");
}
@SafeVarargs
public final HttpPostRequest form(Pair<String, String>... params) {
public final T form(Pair<String, String>... params) {
return form(mapOf(params));
}
public HttpPostRequest string(String payload, String contentType) {
public T string(String payload, String contentType) {
bytes = payload.getBytes(UTF_8);
header("Content-Length", "" + bytes.length);
header("Content-Length", String.valueOf(bytes.length));
contentType(contentType + "; charset=utf-8");
return this;
return (T) this;
}
@Override
public String getString() throws IOException {
return getStringWithRetry(() -> {
HttpURLConnection con = createConnection();
con.setDoOutput(true);
try (OutputStream os = con.getOutputStream()) {
os.write(bytes);
}
URL url = new URL(this.url);
if (con.getResponseCode() / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
try {
throw new ResponseCodeException(url.toString(), con.getResponseCode(), NetworkUtils.readFullyAsString(con));
} catch (IOException e) {
throw new ResponseCodeException(url.toString(), con.getResponseCode(), e);
}
if (bytes != null) {
try (OutputStream os = con.getOutputStream()) {
os.write(bytes);
}
}
checkResponseCode(con);
return NetworkUtils.readFullyAsString(con);
}, retryTimes);
}
}
public static final class HttpPostRequest extends HttpEntityRequest<HttpPostRequest> {
private HttpPostRequest(String url) { super(url, "POST"); }
}
public static final class HttpPutRequest extends HttpEntityRequest<HttpPutRequest> {
private HttpPutRequest(String url) { super(url, "PUT"); }
}
public static HttpGetRequest GET(String url) {
return new HttpGetRequest(url);
}
@@ -216,6 +243,19 @@ public abstract class HttpRequest {
return new HttpPostRequest(url);
}
public static HttpPutRequest PUT(String url) throws MalformedURLException {
return new HttpPutRequest(url);
}
public static HttpDeleteRequest DELETE(String url) {
return new HttpDeleteRequest(url);
}
@SafeVarargs
public static HttpDeleteRequest DELETE(String url, Pair<String, String>... query) {
return DELETE(NetworkUtils.withQuery(url, mapOf(query)));
}
private static String getStringWithRetry(ExceptionalSupplier<String, IOException> supplier, int retryTimes) throws IOException {
Throwable exception = null;
for (int i = 0; i < retryTimes; i++) {
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.ArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
@@ -44,7 +44,7 @@ public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Clos
String name = namePath.toString();
if (name.endsWith(".jar") || name.endsWith(".zip")) {
return new ZipFileTree(new ZipArchiveReader(file));
return CompressingUtils.openZipTree(file);
} else if (name.endsWith(".tar") || name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return TarFileTree.open(file);
} else {
@@ -933,4 +933,21 @@
26.2-pre-6
26.2-rc-1
26.2-rc-2
26.2
26.2
26.3-snapshot-1
26.3-snapshot-2
26.3-snapshot-3
26.3-snapshot-4
26.3-snapshot-5
26.3-snapshot-6
26.3-snapshot-7
26.3-snapshot-8
26.3-snapshot-9
26.3-snapshot-10
26.3-pre-1
26.3-pre-2
26.3-pre-3
26.3-rc-1
26.3-rc-2
26.3-rc-3
26.3
@@ -1,54 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.addon.curse;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.*;
public class CurseForgeRemoteAddonRepositoryTest {
@Test
@Disabled
public void testMurmurHash() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream is = Files.newInputStream(Paths.get("C:\\Users\\huang\\Downloads\\JustEnoughCalculation-1.16.5-3.8.5.jar"))) {
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf, 0, buf.length)) > 0) {
for (int i = 0; i < len; i++) {
byte b = buf[i];
if (b != 9 && b != 10 && b != 13 && b != 32) {
baos.write(b);
}
}
}
}
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
assertEquals(hash, 3333498611L);
}
}
@@ -0,0 +1,73 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.addon.repository;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
/// Tests CurseForge fingerprint calculation for local files.
@NotNullByDefault
public final class CurseForgeRemoteAddonRepositoryTest {
/// Verifies that streaming calculation remains identical to the previous in-memory algorithm.
@Test
public void calculatesFingerprintWithoutChangingTheResult(@TempDir Path tempDir) throws IOException {
byte[] boundarySample = new byte[8192 + 17];
new Random(0).nextBytes(boundarySample);
for (int i = 0; i < boundarySample.length; i += 97) {
boundarySample[i] = 0x20;
}
byte[][] samples = {
{},
{0x9, 0xa, 0xd, 0x20},
{1},
{1, 2},
{1, 2, 3},
{1, 2, 3, 4},
{1, 0x20, 2, 0xa, 3, 0xd, 4, 0x9, 5},
boundarySample
};
for (int i = 0; i < samples.length; i++) {
byte[] sample = samples[i];
Path file = tempDir.resolve("sample-" + i);
Files.write(file, sample);
ByteArrayOutputStream filtered = new ByteArrayOutputStream();
for (byte b : sample) {
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
filtered.write(b);
}
}
long expected = Integer.toUnsignedLong(MurmurHash2.hash32(filtered.toByteArray(), filtered.size(), 1));
assertEquals(expected, CurseForgeRemoteAddonRepository.calculateFingerprint(file));
}
}
}
@@ -0,0 +1,272 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
import java.util.zip.Checksum;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Tests the streaming MurmurHash2 checksum implementation.
@NotNullByDefault
public final class MurmurHash2Test {
/// Compares whole-array, bulk, byte-wise, and fragmented updates across lengths and seeds.
@Test
public void testStreamingHash32AcrossLengthsAndSeeds() {
byte[] data = new byte[513];
new Random(0x6d75726d75724cL).nextBytes(data);
for (int length = 0; length <= data.length; length++) {
assertStreamingHash32(data, length, 0);
assertStreamingHash32(data, length, -1);
assertStreamingHash32(data, length, 0x9747b28c);
assertStreamingHash32(data, length, 0x9747b28c ^ length * 31);
}
}
/// Compares every two-part split while reading from a non-zero source-array offset.
@Test
public void testStreamingHash32AtEverySplitPoint() {
byte[] data = new byte[73];
new Random(0x73706c697473L).nextBytes(data);
int sourceOffset = 5;
byte[] paddedData = new byte[sourceOffset + data.length + 7];
System.arraycopy(data, 0, paddedData, sourceOffset, data.length);
for (int length = 0; length <= data.length; length++) {
int seed = Integer.rotateLeft(0x4d757232, length & 31);
long expected = hash32(data, length, seed);
for (int split = 0; split <= length; split++) {
Checksum checksum = MurmurHash2.hash32(length, seed);
checksum.update(paddedData, sourceOffset, split);
checksum.update(paddedData, sourceOffset + split, length - split);
assertEquals(expected, checksum.getValue(),
"length=" + length + ", split=" + split);
}
}
}
/// Compares interleaved single-byte and bulk updates after every possible short prefix.
@Test
public void testStreamingHash32WithMixedUpdates() {
byte[] data = new byte[129];
new Random(0x6d69786564L).nextBytes(data);
for (int length = 0; length <= data.length; length++) {
int seed = 0x13579bdf ^ length;
long expected = hash32(data, length, seed);
for (int prefixLength = 0; prefixLength <= Math.min(length, 7); prefixLength++) {
Checksum checksum = MurmurHash2.hash32(length, seed);
int offset = 0;
while (offset < prefixLength) {
checksum.update(data[offset++]);
}
// Exercise an empty bulk update both with and without a buffered tail.
checksum.update(data, offset, 0);
while (offset < length) {
int chunkLength = Math.min(offset % 5 + 1, length - offset);
checksum.update(data, offset, chunkLength);
offset += chunkLength;
if (offset < length) {
checksum.update(data[offset++]);
}
}
assertEquals(expected, checksum.getValue(),
"length=" + length + ", prefixLength=" + prefixLength);
}
}
}
/// Compares array-backed, read-only, direct, and empty-buffer updates.
@Test
public void testStreamingHash32WithByteBuffers() {
byte[] data = new byte[8207];
new Random(0x627566666572L).nextBytes(data);
int seed = 0x2468ace0;
long expected = hash32(data, data.length, seed);
int sourceOffset = 3;
byte[] paddedData = new byte[sourceOffset + data.length + 4];
System.arraycopy(data, 0, paddedData, sourceOffset, data.length);
ByteBuffer arrayBackedBuffer = ByteBuffer.wrap(paddedData);
arrayBackedBuffer.position(sourceOffset);
arrayBackedBuffer.limit(sourceOffset + data.length);
assertTrue(arrayBackedBuffer.hasArray());
Checksum arrayBackedChecksum = MurmurHash2.hash32(data.length, seed);
arrayBackedChecksum.update(arrayBackedBuffer);
assertEquals(arrayBackedBuffer.limit(), arrayBackedBuffer.position());
assertEquals(expected, arrayBackedChecksum.getValue());
ByteBuffer readOnlyBuffer = ByteBuffer.wrap(data).asReadOnlyBuffer();
assertFalse(readOnlyBuffer.hasArray());
Checksum readOnlyChecksum = MurmurHash2.hash32(data.length, seed);
readOnlyChecksum.update(readOnlyBuffer);
assertEquals(readOnlyBuffer.limit(), readOnlyBuffer.position());
assertEquals(expected, readOnlyChecksum.getValue());
ByteBuffer directBuffer = ByteBuffer.allocateDirect(sourceOffset + data.length);
directBuffer.position(sourceOffset);
directBuffer.put(data);
directBuffer.position(sourceOffset);
assertFalse(directBuffer.hasArray());
Checksum directChecksum = MurmurHash2.hash32(data.length, seed);
directChecksum.update(directBuffer);
assertEquals(directBuffer.limit(), directBuffer.position());
assertEquals(expected, directChecksum.getValue());
ByteBuffer emptyBuffer = ByteBuffer.allocate(4);
emptyBuffer.position(2);
emptyBuffer.limit(2);
Checksum emptyFirstChecksum = MurmurHash2.hash32(data.length, seed);
emptyFirstChecksum.update(emptyBuffer);
emptyFirstChecksum.update(data);
assertEquals(2, emptyBuffer.position());
assertEquals(expected, emptyFirstChecksum.getValue());
}
/// Verifies that single-byte updates use only the low eight bits of each value.
@Test
public void testStreamingHash32SingleByteValues() {
byte[] data = {0x12, (byte) 0xff, (byte) 0x80, 0x00};
int seed = 0x12345678;
Checksum checksum = MurmurHash2.hash32(data.length, seed);
checksum.update(0x112);
checksum.update(-1);
checksum.update(0xabcdef80);
checksum.update(0x100);
assertEquals(hash32(data, data.length, seed), checksum.getValue());
}
/// Verifies that rejected array ranges leave the checksum state unchanged.
@Test
public void testStreamingHash32ArrayRangeValidation() {
byte[] data = {0x12, 0x34, 0x56};
int seed = 0x12345678;
Checksum checksum = MurmurHash2.hash32(data.length, seed);
assertThrows(NullPointerException.class, () -> checksum.update(null, 0, 0));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, -1, 1));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, 0, -1));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, data.length, 1));
checksum.update(data);
assertEquals(hash32(data, data.length, seed), checksum.getValue());
}
/// Verifies that `getValue()` rejects incomplete and excessive input without finalizing state.
@Test
public void testExactLengthValidation() {
byte[] data = {0x12, 0x34, 0x56, 0x78, (byte) 0x9a};
int seed = 0x12345678;
long expected = hash32(data, data.length, seed);
Checksum checksum = MurmurHash2.hash32(data.length, seed);
checksum.update(data, 0, data.length - 1);
assertThrows(IllegalStateException.class, checksum::getValue);
checksum.update(data[data.length - 1]);
assertEquals(expected, checksum.getValue());
checksum.update(0xff);
checksum.update(0xfe);
assertThrows(IllegalStateException.class, checksum::getValue);
checksum.reset();
checksum.update(data, 0, data.length);
assertEquals(expected, checksum.getValue());
}
/// Verifies the valid range of the declared input length.
@Test
public void testLengthRange() {
assertThrows(IllegalArgumentException.class, () -> MurmurHash2.hash32(-1, 0));
Checksum checksum = MurmurHash2.hash32(1L << 32, 0);
assertThrows(IllegalStateException.class, checksum::getValue);
}
/// Compares the principal update styles with the byte-array implementation.
///
/// @param data the input array
/// @param length the number of bytes to hash
/// @param seed the initial seed value
private static void assertStreamingHash32(byte[] data, int length, int seed) {
long expected = hash32(data, length, seed);
byte[] exactData = Arrays.copyOf(data, length);
Checksum wholeArray = MurmurHash2.hash32(length, seed);
wholeArray.update(exactData);
assertEquals(expected, wholeArray.getValue());
Checksum bulk = MurmurHash2.hash32(length, seed);
bulk.update(data, 0, length);
assertEquals(expected, bulk.getValue());
assertEquals(expected, bulk.getValue());
Checksum byteWise = MurmurHash2.hash32(length, seed);
for (int i = 0; i < length; i++) {
byteWise.update(data[i]);
}
assertEquals(expected, byteWise.getValue());
Checksum fragmented = MurmurHash2.hash32(length, seed);
int offset = 0;
while (offset < length) {
int chunkLength = Math.min(offset % 7 + 1, length - offset);
fragmented.update(data, offset, chunkLength);
offset += chunkLength;
}
assertEquals(expected, fragmented.getValue());
}
/// Returns the unsigned value produced by the byte-array implementation.
///
/// @param data the input array
/// @param length the number of bytes to hash
/// @param seed the initial seed value
/// @return the unsigned 32-bit hash value represented by a `long`
private static long hash32(byte[] data, int length, int seed) {
return Integer.toUnsignedLong(MurmurHash2.hash32(data, length, seed));
}
/// Prevents instantiation.
private MurmurHash2Test() {
}
}
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Tests for installer state stored in [SettingsMap].
@NotNullByDefault
public final class SettingsMapTest {
/// Tests that vanilla game selection alone is not treated as a modded installation.
@Test
public void minecraftSelectionIsNotModdedInstallation() {
SettingsMap settings = new SettingsMap();
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
assertFalse(settings.isInstallingModdedVersion());
}
/// Tests that selecting a non-vanilla installer is treated as a modded installation.
@Test
public void modLoaderSelectionIsModdedInstallation() {
SettingsMap settings = new SettingsMap();
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
settings.put(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), remoteVersion("fabric"));
assertTrue(settings.isInstallingModdedVersion());
}
/// Creates a minimal remote version for installer state tests.
private static RemoteVersion remoteVersion(String libraryId) {
return new RemoteVersion(libraryId, "1.21.11", "test", Instant.EPOCH, List.of());
}
}
+1
View File
@@ -4,6 +4,7 @@ import org.jackhuang.hmcl.gradle.l10n.ParseLanguageSubtagRegistry
plugins {
id("checkstyle")
id("org.glavo.gradle-wrapper-neo") version "0.2.0"
}
group = "org.jackhuang"
+1
View File
@@ -12,6 +12,7 @@ dependencies {
implementation(libs.jna)
implementation(libs.kala.compress.tar)
implementation(libs.kala.compress.ar)
implementation(libs.weburl)
}
java {
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.gradle.mod;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import org.glavo.url.WebURL;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
@@ -33,7 +34,6 @@ import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@@ -58,7 +58,7 @@ public abstract class ParseModDataTask extends DefaultTask {
private static final String MOD_SEPARATOR = ",";
private static final Pattern[] CURSEFORGE_PATTERNS = {
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts)/+(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts|bukkit-plugins)/+(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/projects/(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/mc-mods/minecraft/(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/legacy/mc-mods/minecraft/(\\d+)-(?<modid>[\\w-]+)"),
@@ -70,27 +70,6 @@ public abstract class ParseModDataTask extends DefaultTask {
.replace("&gt;", ">");
}
private static String parseCurseforge(String url) {
URI res = URI.create(url.replace(" ", "%20"));
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
return "";
}
if ("edge.forgecdn.net".equals(res.getHost())) {
return "";
}
for (Pattern pattern : CURSEFORGE_PATTERNS) {
Matcher matcher = pattern.matcher(res.getPath());
if (matcher.matches()) {
return matcher.group("modid");
}
}
return "";
}
private static final Pattern MCMOD_PATTERN =
Pattern.compile("^https://www\\.mcmod\\.cn/(class|modpack)/(?<modid>\\d+)\\.html$");
@@ -169,7 +148,8 @@ public abstract class ParseModDataTask extends DefaultTask {
}
if (chineseName.contains(S) || subName.contains(S)) {
throw new GradleException("Error: " + chineseName);
LOGGER.warn("Error chinese name: {}", chineseName);
continue;
}
String curseforgeId = "";
@@ -180,13 +160,40 @@ public abstract class ParseModDataTask extends DefaultTask {
List<ModData.Link> mcmodLinks = links.get("mcmod");
if (curseforgeLinks != null && !curseforgeLinks.isEmpty()) {
boolean reportError = true;
for (ModData.Link link : curseforgeLinks) {
curseforgeId = parseCurseforge(link.url);
WebURL res = WebURL.parse(link.url);
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
curseforgeId = "";
break;
}
if (Set.of(
"files.xmdhs.com",
"edge.forgecdn.net",
"minecraft.curseforge.com",
"mediafilez.forgecdn.net"
).contains(res.getHost())) {
reportError = false;
curseforgeId = "";
break;
}
for (Pattern pattern : CURSEFORGE_PATTERNS) {
Matcher matcher = pattern.matcher(res.getPath());
if (matcher.matches()) {
curseforgeId = matcher.group("modid");
break;
}
}
if (!curseforgeId.isEmpty()) {
break;
}
}
if (curseforgeId.isEmpty()) {
if (curseforgeId.isEmpty() && reportError) {
LOGGER.warn("Error curseforge: {}", chineseName);
}
}
@@ -50,6 +50,7 @@
"themeColor",
"themeColorStyle",
"titleBarTransparent",
"windowTransparent",
"background",
"backgroundOpacity"
]
@@ -137,6 +138,9 @@
"launcherFontFamily": {
"type": "string"
},
"windowTransparent": {
"type": "boolean"
},
"animationDisabled": {
"type": "boolean"
},
+11
View File
@@ -174,6 +174,9 @@
"titleBar": {
"$ref": "#/$defs/titleBar"
},
"windowTransparent": {
"type": "boolean"
},
"overrides": {
"type": "array",
"items": {
@@ -217,6 +220,11 @@
"required": [
"titleBar"
]
},
{
"required": [
"windowTransparent"
]
}
],
"properties": {
@@ -240,6 +248,9 @@
},
"titleBar": {
"$ref": "#/$defs/titleBar"
},
"windowTransparent": {
"type": "boolean"
}
}
},
+3 -3
View File
@@ -1,7 +1,7 @@
[versions]
hmclauncher = "3.7.0.1"
jetbrains-annotations = "26.1.0"
kala-compress = "1.27.1-3"
kala-compress = "1.27.1-5"
simple-png-javafx = "0.3.0"
gson = "2.13.2"
tomlj = "1.1.1"
@@ -13,11 +13,11 @@ hello-nbt = "0.3.0"
nanohttpd = "2.3.1"
jsoup = "1.21.2"
chardet = "2.5.0"
fxsvgimage = "1.3"
fxsvgimage = "1.9.1"
jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
authlib-injector = "1.2.7"
authlib-injector = "1.2.8"
lwjgl-unsafe-agent = "2.0"
monet-fx = "0.4.0"
terracotta = "0.4.2"
File diff suppressed because it is too large. Load diff
Binary file not shown.
+5 -3
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=120000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
networkTimeout=10000
retries=0
retryBackOffMs=500
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+109 -16
View File
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# Gradle Wrapper Neo startup script for POSIX.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -56,29 +56,29 @@
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
# (3) This file is maintained by the Gradle Wrapper Neo project. It compiles
# gradle/wrapper/GradleWrapperNeo.java on demand and launches the Wrapper
# without storing a Wrapper JAR in the project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
# Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
#
##############################################################################
# Attempt to set APP_HOME
# Determine the directory containing this launcher
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
LAUNCHER_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
*) app_path=$LAUNCHER_HOME$link ;;
esac
done
@@ -86,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
LAUNCHER_HOME=$( cd -P "${LAUNCHER_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -170,7 +170,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
LAUNCHER_HOME=$( cygpath --path --mixed "$LAUNCHER_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -202,17 +202,110 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Select the wrapper entry point. GradleWrapperNeo.java is compiled to a temporary
# classes directory only for the first bootstrap; Java packages the final JAR.
LAUNCHER_WRAPPER_DIR=$LAUNCHER_HOME/gradle/wrapper
APP_HOME=
if [ -f "$LAUNCHER_WRAPPER_DIR/gradle-wrapper.properties" ] ; then
APP_HOME=$LAUNCHER_HOME
else
NEO_SEARCH_START=$( pwd -P )
NEO_SEARCH_DIR=$NEO_SEARCH_START
while [ -z "$APP_HOME" ] ; do
NEO_CANDIDATE_WRAPPER_DIR=$NEO_SEARCH_DIR/gradle/wrapper
if [ -f "$NEO_CANDIDATE_WRAPPER_DIR/gradle-wrapper.properties" ] ; then
APP_HOME=$NEO_SEARCH_DIR
elif [ "$NEO_SEARCH_DIR" = / ] ; then
die "ERROR: Could not find gradle/wrapper/gradle-wrapper.properties searching from $NEO_SEARCH_START."
else
NEO_SEARCH_DIR=${NEO_SEARCH_DIR%/*}
if [ -z "$NEO_SEARCH_DIR" ] ; then
NEO_SEARCH_DIR=/
fi
fi
done
fi
PROJECT_NEO_SOURCE=$APP_HOME/gradle/wrapper/GradleWrapperNeo.java
if [ -f "$PROJECT_NEO_SOURCE" ] ; then
NEO_SOURCE=$PROJECT_NEO_SOURCE
else
NEO_SOURCE=$LAUNCHER_WRAPPER_DIR/GradleWrapperNeo.java
fi
NEO_WORK_DIR=$APP_HOME/.gradle/wrapper-neo
NEO_JAR=$NEO_WORK_DIR/gradle-wrapper-neo.jar
NEO_BOOTSTRAP_DIR=$NEO_WORK_DIR/bootstrap/$$
NEO_CLASSES_DIR=$NEO_BOOTSTRAP_DIR/classes
NEO_JAVA_APP_HOME=$APP_HOME
NEO_JAVA_SOURCE=$NEO_SOURCE
NEO_JAVA_JAR=$NEO_JAR
NEO_JAVA_CLASSES_DIR=$NEO_CLASSES_DIR
if "$cygwin" || "$msys" ; then
NEO_JAVA_APP_HOME=$( cygpath --mixed "$APP_HOME" )
NEO_JAVA_SOURCE=$( cygpath --mixed "$NEO_SOURCE" )
NEO_JAVA_JAR=$( cygpath --mixed "$NEO_JAR" )
NEO_JAVA_CLASSES_DIR=$( cygpath --mixed "$NEO_CLASSES_DIR" )
fi
if [ -f "$NEO_SOURCE" ] ; then
if [ -n "$NEO_JAR" ] && [ -f "$NEO_JAR" ] ; then
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
"-Dorg.gradle.wrapper.neo.app-home=$NEO_JAVA_APP_HOME" \
"-Dorg.gradle.wrapper.neo.source-file=$NEO_JAVA_SOURCE" \
"-Dorg.gradle.wrapper.neo.jar-file=$NEO_JAVA_JAR" \
-jar "$NEO_JAVA_JAR" \
"$@"
else
if [ -n "$JAVA_HOME" ] ; then
JAVACMD_COMPILER=$JAVA_HOME/bin/javac
else
JAVACMD_COMPILER=javac
fi
if ! command -v "$JAVACMD_COMPILER" >/dev/null 2>&1
then
die "ERROR: GradleWrapperNeo.java exists, but javac could not be found.
Please run this wrapper with a JDK so GradleWrapperNeo.java can be compiled."
fi
rm -rf "$NEO_CLASSES_DIR"
mkdir -p "$NEO_CLASSES_DIR" || exit
JAVAC_VERSION=$("$JAVACMD_COMPILER" -version 2>&1)
case "$JAVAC_VERSION" in
javac\ 1.*) JAVAC_TARGET_ARGS="-source 8 -target 8" ;;
*) JAVAC_TARGET_ARGS="--release 8" ;;
esac
# shellcheck disable=SC2086
if ! "$JAVACMD_COMPILER" $JAVAC_TARGET_ARGS -Xlint:-options -encoding UTF-8 -d "$NEO_CLASSES_DIR" "$NEO_SOURCE"
then
rm -rf "$NEO_CLASSES_DIR"
die "ERROR: Could not compile $NEO_SOURCE"
fi
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
"-Dorg.gradle.wrapper.neo.app-home=$NEO_JAVA_APP_HOME" \
"-Dorg.gradle.wrapper.neo.source-file=$NEO_JAVA_SOURCE" \
"-Dorg.gradle.wrapper.neo.jar-file=$NEO_JAVA_JAR" \
"-Dgradle.wrapper.neo.bootstrap=true" \
-cp "$NEO_JAVA_CLASSES_DIR" \
GradleWrapperNeo \
"$@"
fi
else
die "ERROR: $NEO_SOURCE was not found."
fi
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
Vendored
+24 -59
View File
@@ -19,75 +19,40 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem Gradle Wrapper Neo forwarding script for Windows
@rem
@rem This file is maintained by the Gradle Wrapper Neo project. It delegates
@rem to gradlew.ps1, which compiles GradleWrapperNeo.java on demand and
@rem launches the Wrapper without storing a Wrapper JAR in the project.
@rem
@rem Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
setlocal EnableExtensions DisableDelayedExpansion
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
set "POWERSHELL_EXE=pwsh.exe"
"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -Command "exit 0" >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
set "POWERSHELL_EXE=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
if exist "%POWERSHELL_EXE%" goto execute
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
set "POWERSHELL_EXE=powershell.exe"
"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -Command "exit 0" >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo ERROR: PowerShell could not be found. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo Please install Windows PowerShell 5.1 or PowerShell 7 and make it available in PATH. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
endlocal
exit /b 1
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
set "GRADLE_WRAPPER_NEO_BATCH_LAUNCH=1"
set "GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS=%*"
"%POWERSHELL_EXE%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0gradlew.ps1"
set "EXIT_CODE=%ERRORLEVEL%"
endlocal & exit /b %EXIT_CODE%
+382
View File
@@ -0,0 +1,382 @@
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle Wrapper Neo startup script for Windows.
#
# This file is maintained by the Gradle Wrapper Neo project. It compiles
# gradle/wrapper/GradleWrapperNeo.java on demand and launches the Wrapper
# without storing a Wrapper JAR in the project.
#
# Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
#
##############################################################################
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$script:WrapperExitCode = 1
$script:AppBaseName = [IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
$script:LauncherHome = [IO.Path]::GetFullPath($PSScriptRoot)
function ConvertFrom-WindowsCommandLine {
param([string] $CommandLine)
if ($null -eq ('GradleWrapperNeo.NativeCommandLine' -as [type])) {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
namespace GradleWrapperNeo {
public static class NativeCommandLine {
[DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr CommandLineToArgvW(string commandLine, out int argumentCount);
[DllImport("kernel32.dll")]
public static extern IntPtr LocalFree(IntPtr memory);
}
}
"@
}
$nativeCommandLine = 'gradlew.exe ' + $CommandLine
$argumentCount = 0
$argumentVector = [GradleWrapperNeo.NativeCommandLine]::CommandLineToArgvW(
$nativeCommandLine,
[ref] $argumentCount
)
if ($argumentVector -eq [IntPtr]::Zero) {
throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error())
}
try {
$result = New-Object 'System.Collections.Generic.List[string]'
for ($index = 1; $index -lt $argumentCount; $index++) {
$argumentPointer = [Runtime.InteropServices.Marshal]::ReadIntPtr(
$argumentVector,
$index * [IntPtr]::Size
)
[void] $result.Add([Runtime.InteropServices.Marshal]::PtrToStringUni($argumentPointer))
}
return $result.ToArray()
} finally {
[void] [GradleWrapperNeo.NativeCommandLine]::LocalFree($argumentVector)
}
}
function Split-ArgumentString {
param([string] $Value)
if ($null -eq $Value) {
return @()
}
$result = New-Object 'System.Collections.Generic.List[string]'
$currentOption = New-Object Text.StringBuilder
[char] $currentQuote = [char] 0
$insideQuote = $false
$hasOption = $false
for ($index = 0; $index -lt $Value.Length; $index++) {
[char] $character = $Value[$index]
if ((-not $insideQuote) -and [char]::IsWhiteSpace($character)) {
if ($hasOption) {
[void] $result.Add($currentOption.ToString())
[void] $currentOption.Clear()
$hasOption = $false
}
} elseif ((-not $insideQuote) -and (($character -eq [char] 34) -or ($character -eq [char] 39))) {
$currentQuote = $character
$insideQuote = $true
$hasOption = $true
} elseif ($insideQuote -and ($character -eq $currentQuote)) {
$insideQuote = $false
} else {
[void] $currentOption.Append($character)
$hasOption = $true
}
}
if ($hasOption) {
[void] $result.Add($currentOption.ToString())
}
return $result.ToArray()
}
function Resolve-JavaExecutable {
if (-not [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) {
$javaHome = $env:JAVA_HOME.Trim([char] 34)
$javaExecutable = Join-Path $javaHome 'bin\java.exe'
if (-not (Test-Path -LiteralPath $javaExecutable -PathType Leaf)) {
throw "JAVA_HOME is set to an invalid directory: $javaHome"
}
return [IO.Path]::GetFullPath($javaExecutable)
}
$javaCommand = Get-Command -Name 'java.exe' -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -eq $javaCommand) {
throw "JAVA_HOME is not set and no 'java' command could be found in PATH."
}
return $javaCommand.Path
}
function Resolve-JavacExecutable {
if (-not [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) {
$javaHome = $env:JAVA_HOME.Trim([char] 34)
$javacExecutable = Join-Path $javaHome 'bin\javac.exe'
if (Test-Path -LiteralPath $javacExecutable -PathType Leaf) {
return [IO.Path]::GetFullPath($javacExecutable)
}
} else {
$javacCommand = Get-Command -Name 'javac.exe' -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -ne $javacCommand) {
return $javacCommand.Path
}
}
throw 'GradleWrapperNeo.java exists, but javac could not be found. Run this wrapper with a JDK so the source can be compiled.'
}
function Find-AppHome {
param(
[string] $LauncherHome,
[string] $WorkingDirectory
)
$launcherProperties = Join-Path $LauncherHome 'gradle\wrapper\gradle-wrapper.properties'
if (Test-Path -LiteralPath $launcherProperties -PathType Leaf) {
return $LauncherHome
}
$searchDirectory = Get-Item -LiteralPath $WorkingDirectory
$searchStart = $searchDirectory.FullName
while ($null -ne $searchDirectory) {
$propertiesFile = Join-Path $searchDirectory.FullName 'gradle\wrapper\gradle-wrapper.properties'
if (Test-Path -LiteralPath $propertiesFile -PathType Leaf) {
return $searchDirectory.FullName
}
$searchDirectory = $searchDirectory.Parent
}
throw "Could not find gradle/wrapper/gradle-wrapper.properties searching from '$searchStart'."
}
function ConvertTo-JavaPath {
param([string] $Path)
return $Path.Replace('\', '/')
}
function ConvertTo-WindowsCommandLineArgument {
param([AllowEmptyString()][string] $Argument)
if (($Argument.Length -gt 0) -and ($Argument -notmatch '[\s"]')) {
return $Argument
}
$result = New-Object Text.StringBuilder
[void] $result.Append([char] 34)
$backslashCount = 0
for ($index = 0; $index -lt $Argument.Length; $index++) {
[char] $character = $Argument[$index]
if ($character -eq [char] 92) {
$backslashCount++
continue
}
if ($character -eq [char] 34) {
[void] $result.Append(([string] [char] 92) * (($backslashCount * 2) + 1))
} elseif ($backslashCount -gt 0) {
[void] $result.Append(([string] [char] 92) * $backslashCount)
}
$backslashCount = 0
[void] $result.Append($character)
}
if ($backslashCount -gt 0) {
[void] $result.Append(([string] [char] 92) * ($backslashCount * 2))
}
[void] $result.Append([char] 34)
return $result.ToString()
}
function Invoke-NativeApplication {
param(
[string] $Executable,
[string[]] $Arguments,
[string] $WorkingDirectory
)
$quotedArguments = New-Object 'System.Collections.Generic.List[string]'
foreach ($argument in $Arguments) {
[void] $quotedArguments.Add((ConvertTo-WindowsCommandLineArgument -Argument $argument))
}
$startInfo = New-Object Diagnostics.ProcessStartInfo
$startInfo.FileName = $Executable
$startInfo.Arguments = [string]::Join(' ', $quotedArguments.ToArray())
$startInfo.WorkingDirectory = $WorkingDirectory
$startInfo.UseShellExecute = $false
$process = New-Object Diagnostics.Process
$process.StartInfo = $startInfo
try {
if (-not $process.Start()) {
throw "Could not start '$Executable'."
}
$process.WaitForExit()
return $process.ExitCode
} finally {
$process.Dispose()
}
}
function Compile-WrapperSource {
param(
[string] $SourceFile,
[string] $ClassesDirectory,
[string] $BootstrapDirectory
)
$javacExecutable = Resolve-JavacExecutable
[void] [IO.Directory]::CreateDirectory($ClassesDirectory)
$versionOutput = & $javacExecutable -version 2>&1
$versionExitCode = $LASTEXITCODE
if ($versionExitCode -ne 0) {
Remove-Item -LiteralPath $BootstrapDirectory -Recurse -Force -ErrorAction SilentlyContinue
throw "Could not run javac at '$javacExecutable'."
}
$versionText = ($versionOutput | Out-String).Trim()
if ($versionText.StartsWith('javac 1.')) {
[string[]] $targetArguments = @('-source', '8', '-target', '8')
} else {
[string[]] $targetArguments = @('--release', '8')
}
[string[]] $compileArguments = @(
$targetArguments
'-Xlint:-options'
'-encoding'
'UTF-8'
'-d'
$ClassesDirectory
$SourceFile
)
& $javacExecutable @compileArguments
$compileExitCode = $LASTEXITCODE
if ($compileExitCode -ne 0) {
Remove-Item -LiteralPath $BootstrapDirectory -Recurse -Force -ErrorAction SilentlyContinue
throw "Could not compile '$SourceFile'."
}
}
function Invoke-GradleWrapper {
param([string[]] $GradleArguments)
$currentLocation = Get-Location
if ($currentLocation.Provider.Name -ne 'FileSystem') {
throw "The current location is not a file system directory: $currentLocation"
}
$workingDirectory = $currentLocation.ProviderPath
$appHome = Find-AppHome -LauncherHome $script:LauncherHome -WorkingDirectory $workingDirectory
$launcherWrapperDirectory = Join-Path $script:LauncherHome 'gradle\wrapper'
$projectSource = Join-Path $appHome 'gradle\wrapper\GradleWrapperNeo.java'
if (Test-Path -LiteralPath $projectSource -PathType Leaf) {
$sourceFile = $projectSource
} else {
$sourceFile = Join-Path $launcherWrapperDirectory 'GradleWrapperNeo.java'
}
if (-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) {
throw "Wrapper source file '$sourceFile' was not found."
}
$workDirectory = Join-Path $appHome '.gradle\wrapper-neo'
$jarFile = Join-Path $workDirectory 'gradle-wrapper-neo.jar'
$bootstrapDirectory = Join-Path (Join-Path $workDirectory 'bootstrap') ([Guid]::NewGuid().ToString('N'))
$classesDirectory = Join-Path $bootstrapDirectory 'classes'
$javaExecutable = Resolve-JavaExecutable
[string[]] $jvmArguments = @('-Xmx64m', '-Xms64m')
foreach ($optionSource in @($env:JAVA_OPTS, $env:GRADLE_OPTS)) {
foreach ($option in @(Split-ArgumentString -Value $optionSource)) {
$jvmArguments += $option
}
}
$javaAppHome = ConvertTo-JavaPath -Path $appHome
$javaSourceFile = ConvertTo-JavaPath -Path $sourceFile
$javaJarFile = ConvertTo-JavaPath -Path $jarFile
$javaClassesDirectory = ConvertTo-JavaPath -Path $classesDirectory
[string[]] $javaArguments = @(
$jvmArguments
"-Dorg.gradle.appname=$script:AppBaseName"
"-Dorg.gradle.wrapper.neo.app-home=$javaAppHome"
"-Dorg.gradle.wrapper.neo.source-file=$javaSourceFile"
"-Dorg.gradle.wrapper.neo.jar-file=$javaJarFile"
)
$cachedJar = Get-Item -LiteralPath $jarFile -ErrorAction SilentlyContinue
if (($null -ne $cachedJar) -and ($cachedJar.Length -eq 0)) {
Remove-Item -LiteralPath $jarFile -Force -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $jarFile -PathType Leaf) {
$javaArguments += @('-jar', $javaJarFile)
} else {
Compile-WrapperSource -SourceFile $sourceFile -ClassesDirectory $classesDirectory -BootstrapDirectory $bootstrapDirectory
$javaArguments += @(
'-Dgradle.wrapper.neo.bootstrap=true'
'-cp'
$javaClassesDirectory
'GradleWrapperNeo'
)
}
$javaArguments += $GradleArguments
$script:WrapperExitCode = Invoke-NativeApplication `
-Executable $javaExecutable `
-Arguments $javaArguments `
-WorkingDirectory $workingDirectory
}
try {
if ($env:GRADLE_WRAPPER_NEO_BATCH_LAUNCH -eq '1') {
$rawArguments = $env:GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS
Remove-Item Env:GRADLE_WRAPPER_NEO_BATCH_LAUNCH -ErrorAction SilentlyContinue
Remove-Item Env:GRADLE_WRAPPER_NEO_BATCH_ARGUMENTS -ErrorAction SilentlyContinue
[string[]] $gradleArguments = @(ConvertFrom-WindowsCommandLine -CommandLine $rawArguments)
} else {
[string[]] $gradleArguments = @($args)
}
Invoke-GradleWrapper -GradleArguments $gradleArguments
exit $script:WrapperExitCode
} catch {
[Console]::Error.WriteLine()
[Console]::Error.WriteLine('ERROR: ' + $_.Exception.Message)
[Console]::Error.WriteLine()
exit 1
}