Compare commits

...
24 Commits
Author SHA1 Message Date
辞庐 91a613d966 HintPane 使用新版 Bootstrap 调色盘 (#4323) 2025-08-24 22:30:14 +08:00
Glavo 06b7b1e574 Fix #4325: 修复未将 15w14a 识别为愚人节版本的问题 (#4327) 2025-08-24 22:27:49 +08:00
Glavo 933b7cab2f 将 URI 中的错误代理对编码为 U+FFFD (#4329) 2025-08-24 22:27:18 +08:00
Glavo 885e25d07a 更新文档 (#4328) 2025-08-24 20:33:13 +08:00
Glavo 421f9a3b2b 支持以字符串形式序列化 VersionSetting 中的枚举成员 (#4299) 2025-08-21 21:56:49 +08:00
Glavo e52cfb036e 优化 Task.getCaller() (#4308) 2025-08-21 20:53:19 +08:00
3gf8jv4dv 87a3a4ece3 chore(locale): add new translations for russian and spanish (#4304) 2025-08-21 20:45:40 +08:00
3gf8jv4dv b152e2bfab fix(locale): add missing i18n when installing an instance with quilt api (#4303) 2025-08-21 20:45:08 +08:00
Glavo c9c97a45b7 修复 TaskListPane 中任务总数显示错误的问题 (#4310) 2025-08-21 20:43:19 +08:00
Glavo cba4ba2799 删除无用的 debug 日志 (#4309) 2025-08-21 20:42:51 +08:00
3gf8jv4dv ab9eea3620 fix(locale): revise mcmod-info-mirror description on thanks list (#4305) 2025-08-21 20:42:22 +08:00
Glavoand3gf8jv4dv 96f2d011ca 优化 TaskListPane 的显示效果 (#4306)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-21 16:45:07 +08:00
Glavo 9ee55cd17d 修复 ModrinthModpackExportTask 名称未被翻译的问题 (#4301) 2025-08-20 21:47:56 +08:00
Glavo 07ee7867da 修复 VersionIconType 成员的排列顺序 (#4298) 2025-08-20 21:20:31 +08:00
Glavo 940d4f722c 修复 TaskListPane 可能出现横向滚动条的问题 (#4297) 2025-08-20 20:34:12 +08:00
Glavo af56096162 优化对于 IntelliJ IDEA 的支持 (#4270) 2025-08-20 20:33:03 +08:00
Glavo 0e12c1a132 Fix #4290: 在 macOS 上禁止最大化和全屏模式 (#4293) 2025-08-20 15:40:48 +08:00
Glavo ca20509cb1 优化 TaskListPane (#4292) 2025-08-20 00:32:40 +08:00
HenryxjhandGlavo 58c93ff349 修复 Windows 平台 GPU 信息可能显示为 "[B@xxx" 的问题 (#4288)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-19 21:33:10 +08:00
Glavo aab3b22290 修复 TaskListPane 未正确处理 StagesHintTask 的问题 (#4285) 2025-08-19 16:22:51 +08:00
Wulian233 6cf933741f 基于实际屏幕分辨率添加游戏窗口分辨率选项 (#4275) 2025-08-18 20:05:34 +08:00
Glavo 9d17834227 Fix #4281: 支持解析 Indev 版本号 (#4282) 2025-08-18 20:04:22 +08:00
Glavo 7805508d4c Fix #2676: 优化版本号排序规则 (#4276) 2025-08-18 16:24:03 +08:00
Wulian233 72f6c2faf3 修复 1.21.5 以上的地图跳转种子地图错误 (#4274) 2025-08-18 16:21:07 +08:00
44 changed files with 673 additions and 364 deletions

No files matched your search

+73 -37
View File
@@ -108,6 +108,50 @@ tasks.compileJava {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
}
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile by tasks.registering {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
doLast {
val targetFile = hmclPropertiesFile.get().asFile
targetFile.parentFile.mkdir()
targetFile.bufferedWriter().use {
for ((k, v) in hmclProperties) {
it.write("$k=$v\n")
}
}
}
}
val addOpens = listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)
tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
@@ -116,6 +160,8 @@ tasks.jar {
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.shadowJar {
dependsOn(createPropertiesFile)
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
@@ -136,43 +182,14 @@ tasks.shadowJar {
exclude(project(":HMCLBoot"))
}
into("assets/") {
from(embedResources)
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Authlib-Injector-Version" to libs.authlib.injector.get().version!!,
"Build-Channel" to versionType,
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
).joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED"
)
System.getenv("GITHUB_SHA")?.also {
attributes("GitHub-SHA" to it)
}
}
manifest.attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Implementation-Version" to project.version.toString(),
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Add-Opens" to addOpens.joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED"
)
if (launcherExe != null) {
into("assets") {
@@ -186,6 +203,15 @@ tasks.shadowJar {
}
}
tasks.processResources {
dependsOn(createPropertiesFile)
into("assets/") {
from(hmclPropertiesFile)
from(embedResources)
}
}
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
@@ -268,6 +294,16 @@ fun parseToolOptions(options: String?): MutableList<String> {
return result
}
// For IntelliJ IDEA
tasks.withType<JavaExec> {
if (name != "run") {
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
if (javaVersion >= JavaVersion.VERSION_24) {
jvmArgs("--enable-native-access=ALL-UNNAMED")
}
}
}
tasks.register<JavaExec>("run") {
dependsOn(tasks.jar)
@@ -33,7 +33,7 @@ public final class Metadata {
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
@@ -49,8 +49,8 @@ public final class Metadata {
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
@@ -50,10 +50,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -210,14 +207,17 @@ public final class ModpackHelper {
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
@@ -158,13 +158,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
}
@Override
@@ -363,9 +363,9 @@ public final class Accounts {
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
String authlibInjectorVersion = JarUtils.getManifestAttribute("Authlib-Injector-Version", null);
String authlibInjectorVersion = JarUtils.getAttribute("hmcl.authlib-injector.version", null);
if (authlibInjectorVersion == null)
throw new AssertionError("Missing Authlib-Injector-Version");
throw new AssertionError("Missing hmcl.authlib-injector.version");
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
@@ -29,12 +29,12 @@ public enum VersionIconType {
COMMAND("/assets/img/command.png"),
OPTIFINE("/assets/img/optifine.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
APRIL_FOOLS("/assets/img/april_fools.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
QUILT("/assets/img/quilt.png"),
APRIL_FOOLS("/assets/img/april_fools.png");
// Please append new items at last
@@ -797,14 +797,6 @@ public final class VersionSetting implements Cloneable, Observable {
return obj;
}
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
if (index == null)
return defaultValue;
int idx = index.getAsInt();
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
}
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject))
@@ -839,14 +831,14 @@ public final class VersionSetting implements Cloneable, Observable {
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
vs.setGameDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("gameDirType"), GameDirectoryType.class, GameDirectoryType.ROOT_FOLDER));
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
vs.setNativesDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("nativesDirType"), NativesDirectoryType.class, NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(parseJsonPrimitive(obj.getAsJsonPrimitive("versionIcon"), VersionIconType.class, VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
@@ -217,8 +217,10 @@ public final class Controllers {
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()
&& !Controllers.stage.isFullScreen()
&& !Controllers.stage.isMaximized()) {
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
) {
targetProperty.set(sourceProperty.get());
}
};
@@ -22,7 +22,6 @@ import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -59,16 +58,10 @@ public class TaskExecutorDialogPane extends BorderPane {
lblTitle = new Label();
lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
{
taskListPane = new TaskListPane();
scrollPane.setContent(taskListPane);
}
taskListPane = new TaskListPane();
VBox.setVgrow(taskListPane, Priority.ALWAYS);
center.getChildren().setAll(lblTitle, scrollPane);
center.getChildren().setAll(lblTitle, taskListPane);
}
BorderPane bottom = new BorderPane();
@@ -17,15 +17,20 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
@@ -52,6 +57,7 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthCompletionTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthInstallTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
@@ -63,54 +69,86 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.FXThread;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
private TaskExecutor executor;
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXListView<Node> listView = new JFXListView<>();
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
private final List<StageNode> stageNodes = new ArrayList<>();
private final Map<String, StageNode> stageNodes = new HashMap<>();
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
private final DoubleProperty cellWidth = new SimpleDoubleProperty();
private Cell lastCell;
public TaskListPane() {
listBox.setSpacing(0);
listView.setPadding(new Insets(12, 0, 0, 0));
listView.setCellFactory(l -> new Cell());
listView.setSelectionModel(null);
FXUtils.onChangeAndOperate(listView.widthProperty(), width -> {
double w = width.doubleValue();
cellWidth.set(w <= 12.0 ? w : w - 12.0);
});
getChildren().setAll(listBox);
getChildren().setAll(listView);
}
@FXThread
private void addStages(@NotNull Collection<String> stages) {
for (String stage : stages) {
stageNodes.computeIfAbsent(stage, s -> {
StageNode node = new StageNode(stage);
listView.getItems().add(node);
return node;
});
}
}
@FXThread
private void updateProgressNodePadding() {
progressNodePadding.set(stageNodes.isEmpty() ? DEFAULT_PROGRESS_NODE_PADDING : STAGED_PROGRESS_NODE_PADDING);
}
public void setExecutor(TaskExecutor executor) {
List<String> stages = Lang.removingDuplicates(executor.getStages());
this.executor = executor;
executor.addTaskListener(new TaskListener() {
@Override
public void onStart() {
Platform.runLater(() -> {
stageNodes.clear();
listBox.clear();
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
stageNodes.forEach(listBox::add);
if (stages.isEmpty()) progressNodePadding.setValue(new Insets(0, 0, 8, 0));
else progressNodePadding.setValue(new Insets(0, 0, 8, 26));
listView.getItems().clear();
addStages(executor.getStages());
updateProgressNodePadding();
});
}
@Override
public void onReady(Task<?> task) {
if (task instanceof Task.StagesHintTask) {
Platform.runLater(() -> {
addStages(((Task<?>.StagesHintTask) task).getStages());
updateProgressNodePadding();
});
}
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
StageNode node = stageNodes.get(task.getStage());
if (node != null)
node.begin();
});
}
}
@@ -123,6 +161,8 @@ public final class TaskListPane extends StackPane {
if (task instanceof GameAssetDownloadTask) {
task.setName(i18n("assets.download_all"));
} else if (task instanceof GameInstallTask) {
if (task.getInheritedStage() != null && task.getInheritedStage().startsWith("hmcl.install.game"))
return;
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
@@ -156,7 +196,7 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
@@ -169,25 +209,25 @@ public final class TaskListPane extends StackPane {
Platform.runLater(() -> {
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
listBox.add(listBox.indexOf(stageNode) + 1, node);
StageNode stageNode = stageNodes.get(task.getInheritedStage());
listView.getItems().add(listView.getItems().indexOf(stageNode) + 1, node);
});
}
@Override
public void onFinished(Task<?> task) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
});
}
Platform.runLater(() -> {
if (task.getStage() != null) {
StageNode stageNode = stageNodes.get(task.getStage());
if (stageNode != null)
stageNode.succeed();
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
listBox.remove(node);
if (node != null) {
node.unbind();
listView.getItems().remove(node);
}
});
}
@@ -195,25 +235,23 @@ public final class TaskListPane extends StackPane {
public void onFailed(Task<?> task, Throwable throwable) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
StageNode stageNode = stageNodes.get(task.getStage());
if (stageNode != null)
stageNode.fail();
});
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
Platform.runLater(() -> {
node.setThrowable(throwable);
});
if (node != null)
Platform.runLater(() -> node.setThrowable(throwable));
}
@Override
public void onPropertiesUpdate(Task<?> task) {
if (task instanceof Task.CountTask) {
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(((Task<?>.CountTask) task).getCountStage()))
.findAny()
.ifPresent(StageNode::count);
StageNode stageNode = stageNodes.get(((Task<?>.CountTask) task).getCountStage());
if (stageNode != null)
stageNode.count();
});
return;
@@ -222,121 +260,251 @@ public final class TaskListPane extends StackPane {
if (task.getStage() != null) {
int total = tryCast(task.getProperties().get("total"), Integer.class).orElse(0);
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(task.getStage()))
.findAny()
.ifPresent(stageNode -> {
stageNode.setTotal(total);
});
StageNode stageNode = stageNodes.get(task.getStage());
if (stageNode != null)
stageNode.addTotal(total);
});
}
}
});
}
private static class StageNode extends BorderPane {
private final String stage;
private final class Cell extends ListCell<Node> {
private static final double STATUS_ICON_SIZE = 14;
private final BorderPane pane = new BorderPane();
private final StackPane left = new StackPane();
private final Label title = new Label();
private final Label message = new Label();
private final JFXProgressBar bar = new JFXProgressBar();
private WeakReference<StageNode> prevStageNodeRef;
private ChangeListener<StageNode.Status> statusChangeListener;
private Cell() {
setPadding(Insets.EMPTY);
prefWidthProperty().bind(cellWidth);
FXUtils.setLimitHeight(left, STATUS_ICON_SIZE);
FXUtils.setLimitWidth(left, STATUS_ICON_SIZE);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
BorderPane.setMargin(left, new Insets(0, 12, 0, 0));
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
pane.setCenter(title);
DoubleBinding barWidth = Bindings.createDoubleBinding(() -> {
Insets padding = pane.getPadding();
Insets insets = pane.getInsets();
return pane.getWidth() - padding.getLeft() - padding.getRight() - insets.getLeft() - insets.getRight();
}, pane.paddingProperty(), pane.widthProperty());
bar.minWidthProperty().bind(barWidth);
bar.prefWidthProperty().bind(barWidth);
bar.maxWidthProperty().bind(barWidth);
setGraphic(pane);
}
private void updateLeftIcon(StageNode.Status status) {
left.getChildren().setAll(status.svg.createIcon(Theme.blackFill(), STATUS_ICON_SIZE));
}
@Override
protected void updateItem(Node item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell && !isVisible())
return;
lastCell = this;
pane.paddingProperty().unbind();
title.textProperty().unbind();
message.textProperty().unbind();
bar.progressProperty().unbind();
StageNode prevStageNode;
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
prevStageNode.status.removeListener(statusChangeListener);
if (item instanceof ProgressListNode) {
var progressListNode = (ProgressListNode) item;
title.setText(progressListNode.title);
message.textProperty().bind(progressListNode.message);
bar.progressProperty().bind(progressListNode.progress);
pane.paddingProperty().bind(progressNodePadding);
pane.setLeft(null);
pane.setRight(message);
pane.setBottom(bar);
} else if (item instanceof StageNode) {
var stageNode = (StageNode) item;
title.textProperty().bind(stageNode.title);
message.setText("");
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
pane.setPadding(Insets.EMPTY);
pane.setLeft(left);
pane.setRight(message);
pane.setBottom(null);
updateLeftIcon(stageNode.status.get());
if (statusChangeListener == null)
statusChangeListener = new StatusChangeListener(this);
stageNode.status.addListener(statusChangeListener);
prevStageNodeRef = new WeakReference<>(stageNode);
} else { // item == null
title.setText("");
message.setText("");
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
pane.setPadding(Insets.EMPTY);
pane.setLeft(null);
pane.setRight(null);
pane.setBottom(null);
}
}
private final class StatusChangeListener implements ChangeListener<StageNode.Status>, WeakListener {
private final WeakReference<Cell> cellRef;
private StatusChangeListener(Cell cell) {
this.cellRef = new WeakReference<>(cell);
}
@Override
public boolean wasGarbageCollected() {
return cellRef.get() == null;
}
@Override
public void changed(ObservableValue<? extends StageNode.Status> observable,
StageNode.Status oldValue,
StageNode.Status newValue) {
Cell cell = cellRef.get();
if (cell == null) {
if (observable != null)
observable.removeListener(this);
return;
}
cell.updateLeftIcon(newValue);
}
}
}
private static abstract class Node {
}
private static final class StageNode extends Node {
private enum Status {
WAITING(SVG.MORE_HORIZ),
RUNNING(SVG.ARROW_FORWARD),
SUCCESS(SVG.CHECK),
FAILED(SVG.CLOSE);
private final SVG svg;
Status(SVG svg) {
this.svg = svg;
}
}
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.WAITING);
private final StringProperty title = new SimpleStringProperty();
private final String message;
private int count = 0;
private int total = 0;
private boolean started = false;
public StageNode(String stage) {
this.stage = stage;
private StageNode(String stage) {
String stageKey;
String stageValue;
String stageKey = StringUtils.substringBefore(stage, ':');
String stageValue = StringUtils.substringAfter(stage, ':');
int idx = stage.indexOf(':');
if (idx >= 0) {
stageKey = stage.substring(0, idx);
stageValue = stage.substring(idx + 1);
} else {
stageKey = stage;
stageValue = null;
}
// CHECKSTYLE:OFF
// @formatter:off
switch (stageKey) {
case "hmcl.modpack": message = i18n("install.modpack"); break;
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
case "hmcl.install.assets": message = i18n("assets.download"); break;
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
case "hmcl.modpack": message = i18n("install.modpack"); break;
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
case "hmcl.install.assets": message = i18n("assets.download"); break;
case "hmcl.install.libraries": message = i18n("libraries.download"); break;
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
default: message = i18n(stageKey); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
// CHECKSTYLE:ON
title.setText(message);
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
setPadding(new Insets(0, 0, 8, 4));
setCenter(title);
setLeft(FXUtils.limitingSize(SVG.MORE_HORIZ.createIcon(Theme.blackFill(), 14), 14, 14));
title.set(message);
}
public void begin() {
if (started) return;
started = true;
setLeft(FXUtils.limitingSize(SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), 14), 14, 14));
private void begin() {
if (status.get() == Status.WAITING) {
status.set(Status.RUNNING);
}
}
public void fail() {
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14), 14, 14));
status.set(Status.FAILED);
}
public void succeed() {
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14), 14, 14));
status.set(Status.SUCCESS);
}
public void count() {
updateCounter(++count, total);
}
public void setTotal(int total) {
this.total = total;
public void addTotal(int n) {
this.total += n;
updateCounter(count, total);
}
public void updateCounter(int count, int total) {
if (total > 0)
title.setText(String.format("%s - %d/%d", message, count, total));
else
title.setText(message);
title.setValue(total > 0
? message + " - " + count + "/" + total
: message
);
}
}
private class ProgressListNode extends BorderPane {
private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label();
private final Label state = new Label();
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
getWidth() - getPadding().getLeft() - getPadding().getRight() - getInsets().getLeft() - getInsets().getRight(),
paddingProperty(), widthProperty());
private static final class ProgressListNode extends Node {
private final String title;
private final StringProperty message = new SimpleStringProperty("");
private final DoubleProperty progress = new SimpleDoubleProperty(0.0);
public ProgressListNode(Task<?> task) {
bar.progressProperty().bind(task.progressProperty());
title.setText(task.getName());
state.textProperty().bind(task.messageProperty());
setLeft(title);
setRight(state);
setBottom(bar);
bar.minWidthProperty().bind(binding);
bar.prefWidthProperty().bind(binding);
bar.maxWidthProperty().bind(binding);
paddingProperty().bind(progressNodePadding);
private ProgressListNode(Task<?> task) {
this.title = task.getName();
message.bind(task.messageProperty());
progress.bind(task.progressProperty());
}
public void unbind() {
bar.progressProperty().unbind();
state.textProperty().unbind();
message.unbind();
progress.unbind();
}
public void setThrowable(Throwable throwable) {
unbind();
state.setText(throwable.getLocalizedMessage());
bar.setProgress(0);
message.set(throwable.getLocalizedMessage());
progress.set(0.);
}
}
}
@@ -58,6 +58,7 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -164,13 +165,16 @@ public class DecoratorController {
// press ESC to go back
onEscPressed(navigator, this::back);
// press F11 to toggle full screen
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
e.consume();
}
});
// https://github.com/HMCL-dev/HMCL/issues/4290
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
// press F11 to toggle full screen
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
e.consume();
}
});
}
try {
// For JavaFX 12+
@@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
public class DecoratorSkin extends SkinBase<Decorator> {
private final StackPane root, parent;
@@ -89,22 +90,31 @@ public class DecoratorSkin extends SkinBase<Decorator> {
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
onWindowsStatusChange = observable -> {
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
} else {
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
}
};
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
onWindowsStatusChange.invalidated(null);
// https://github.com/HMCL-dev/HMCL/issues/4290
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
onWindowsStatusChange = observable -> {
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
} else {
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
}
};
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
onWindowsStatusChange.invalidated(null);
} else {
onWindowsStatusChange = null;
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
}
shadowContainer.getChildren().setAll(parent);
root.getChildren().setAll(shadowContainer);
@@ -177,12 +187,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
BorderPane titleBar = new BorderPane();
titleContainer.getChildren().add(titleBar);
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
primaryStage.setMaximized(!primaryStage.isMaximized());
event.consume();
}
});
// https://github.com/HMCL-dev/HMCL/issues/4290
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
primaryStage.setMaximized(!primaryStage.isMaximized());
event.consume();
}
});
}
Rectangle buttonsContainerPlaceHolder = new Rectangle();
{
@@ -85,6 +85,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
if ("game".equals(remoteVersion.getLibraryId())) {
stages.add("hmcl.install.libraries");
stages.add("hmcl.install.assets");
}
} else if (value instanceof RemoveVersionAction) {
@@ -200,7 +200,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
case PENDING:
case SNAPSHOT:
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isSpecial()) {
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
content.getTags().setAll(i18n("version.game.april_fools"));
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
} else {
@@ -492,7 +492,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|| versionType == RemoteVersion.Type.PENDING;
case APRIL_FOOLS:
return versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isSpecial();
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
case OLD:
return versionType == RemoteVersion.Type.OLD;
case ALL:
@@ -77,6 +77,10 @@ public final class ExportWizardProvider implements WizardProvider {
File tempModpack;
Task<?> exportTask;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public boolean doPreExecute() {
return true;
@@ -165,6 +169,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency = null;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
@@ -181,6 +189,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
VersionSetting vs = profile.getVersionSetting(version);
@@ -225,6 +237,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
@@ -241,6 +257,10 @@ public final class ExportWizardProvider implements WizardProvider {
return new Task<Void>() {
Task<?> dependency;
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() {
dependency = new ModrinthModpackExportTask(
@@ -70,7 +70,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -548,7 +547,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
LOG.debug("Icon: " + dataItem.getIconUrl());
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
}
}
@@ -26,12 +26,14 @@ import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Screen;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
@@ -394,14 +396,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
cboWindowsSize.setEditable(true);
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
cboWindowsSize.setPromptText("854x480");
cboWindowsSize.getItems().addAll(
"854x480",
"1280x720",
"1600x900",
"1920x1080",
"2560x1440",
"3840x2160"
);
cboWindowsSize.getItems().setAll(getSupportedResolutions());
chkFullscreen = new JFXCheckBox();
right.setRight(chkFullscreen);
@@ -784,6 +779,28 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
private static List<String> getSupportedResolutions() {
int maxScreenWidth = 0;
int maxScreenHeight = 0;
for (Screen screen : Screen.getScreens()) {
Rectangle2D bounds = screen.getBounds();
int screenWidth = (int) (bounds.getWidth() * screen.getOutputScaleX());
int screenHeight = (int) (bounds.getHeight() * screen.getOutputScaleY());
maxScreenWidth = Math.max(maxScreenWidth, screenWidth);
maxScreenHeight = Math.max(maxScreenHeight, screenHeight);
}
List<String> resolutions = new ArrayList<>(List.of("854x480", "1280x720", "1600x900"));
if (maxScreenWidth >= 1920 && maxScreenHeight >= 1080) resolutions.add("1920x1080");
if (maxScreenWidth >= 2560 && maxScreenHeight >= 1440) resolutions.add("2560x1440");
if (maxScreenWidth >= 3840 && maxScreenHeight >= 2160) resolutions.add("3840x2160");
return resolutions;
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
@@ -30,9 +30,9 @@ public final class ChunkBaseApp {
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3", "1.19",
"1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12", "1.11",
"1.10", "1.9", "1.8", "1.7"
"1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3",
"1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12",
"1.11", "1.10", "1.9", "1.8", "1.7"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
@@ -46,7 +46,6 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.Platform;
@@ -63,6 +62,7 @@ import java.util.List;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Manifest;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
@@ -261,9 +261,15 @@ public final class SelfDependencyPatcher {
.map(DependencyDescriptor::localPath)
.toArray(Path[]::new);
String[] addOpens = JarUtils.getManifestAttribute("Add-Opens", "").split(" ");
String addOpens = null;
try (InputStream input = SelfDependencyPatcher.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
if (input != null)
addOpens = new Manifest(input).getMainAttributes().getValue("Add-Opens");
} catch (IOException e) {
LOG.warning("Failed to read MANIFEST.MF file", e);
}
JavaFXPatcher.patch(modules, jars, addOpens);
JavaFXPatcher.patch(modules, jars, addOpens != null ? addOpens.split(" ") : new String[0]);
}
/**
+11 -11
View File
@@ -52,43 +52,43 @@
/*
Colors are picked from bootstrap
https://getbootstrap.com/docs/4.1/components/alerts/
https://getbootstrap.com/docs/5.3/components/alerts/
*/
.hint.info {
-fx-background-color: #cce5ff;
-fx-border-color: #b8daff;
-fx-background-color: #cfe2ff;
-fx-border-color: #9ec5fe;
}
.hint.info Text, .hint.info .svg {
-fx-fill: #004085;
-fx-fill: #052c65;
}
.hint.success {
-fx-background-color: #d4edda;
-fx-border-color: #c3e6cb;
-fx-background-color: #d1e7dd;
-fx-border-color: #a3cfbb;
}
.hint.success Text, .hint.success .svg {
-fx-fill: #155724;
-fx-fill: #0a3622;
}
.hint.error {
-fx-background-color: #f8d7da;
-fx-border-color: #f5c6cb;
-fx-border-color: #f1aeb5;
}
.hint.error Text, .hint.error .svg {
-fx-fill: #721c24;
-fx-fill: #58151c;
}
.hint.warning {
-fx-background-color: #fff3cd;
-fx-border-color: #ffeeba;
-fx-border-color: #ffe69c;
}
.hint.warning Text, .hint.warning .svg {
-fx-fill: #856404;
-fx-fill: #664d03;
}
.skin-pane .jfx-text-field {
@@ -40,7 +40,7 @@ about.thanks_to.zkitefly.statement=Responsible for maintaining the documentation
about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
about.thanks_to.mcbbs.statement=For providing the mcbbs.net download mirror for Chinese Mainland users. (No longer available)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=Provides mod information cache acceleration service
about.thanks_to.mcim.statement=For providing the mod info cache acceleration service for Chinese Mainland users.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=For providing the Simplified Chinese translations and wiki for various mods.
about.thanks_to.red_lnn.statement=For providing the default background image.
@@ -827,6 +827,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a f
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher!
launcher.update_java=Please update your Java version.
libraries.download=Downloading Libraries
login.empty_username=You have not set your username yet!
login.enter_password=Please enter your password.
@@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Aportar mucho apoyo técnico a HMCL.
about.thanks_to.zkitefly.statement=Responsable del mantenimiento de la documentación de HMCL.
about.thanks_to.mcbbs=MCBBS (Foro Chino Minecraft)
about.thanks_to.mcbbs.statement=Por proporcionar el espejo de descarga de mcbbs.net para los usuarios de China continental. (Ya no disponible)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=Por proporcionar el servicio de aceleración de caché de información de mods para los usuarios de China continental.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=Por proporcionar las traducciones al chino simplificado y la wiki de varios mods.
about.thanks_to.red_lnn.statement=Por proporcionar la imagen de fondo por defecto.
@@ -719,7 +721,7 @@ install.installer.external_version=%s (Instalado por un proceso externo, que no
install.installing=Instalando
install.modpack=Añadir modpack
install.modpack.installation=Instalación de modpack
install.name.invalid=El nombre contiene caracteres no ASCII (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
install.name.invalid=El nombre contiene caracteres especiales (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
install.new_game=Añadir instancia
install.new_game.already_exists=Esta instancia ya existe. Por favor, utilice otro nombre.
install.new_game.current_game_version=Versión actual de la instancia
@@ -830,6 +832,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher ha encontrado un er
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher ha encontrado un error fatal. Su launcher está desactualizado. Por favor, ¡actualícelo!
launcher.update_java=Por favor, actualice su versión de Java.
libraries.download=Descargando bibliotecas
login.empty_username=¡Todavía no has puesto tu nombre de usuario!
login.enter_password=Por favor, introduzca su contraseña.
@@ -1426,12 +1430,18 @@ version.empty=No hay instancias
version.empty.add=Añadir una instancia
version.empty.launch=No hay instancias disponibles. Si haces clic en «Aceptar», accederás a la página «Descargar». También puedes descargar el juego o cambiar de directorio de juego con los botones «Descargar» o «Todas las instancias» de la página principal de HMCL.
version.empty.hint=No hay instancias de Minecraft aquí.\nPuedes intentar cambiar a otro directorio del juego o hacer clic aquí para descargar una.
version.game.all=Todos
version.game.april_fools=Día de los Inocentes
version.game.old=Histórico
version.game.release=Lanzamiento
version.game.releases=Lanzamientos
version.game.snapshot=Snapshot
version.game.snapshots=Snapshots
version.game.type=Tipo
version.launch=Iniciar Minecraft
version.launch.empty=Iniciar Minecraft
version.launch.empty.installing=Instalando el juego
version.launch.empty.tooltip=Instala y ejecuta la última versión oficial del juego.
version.launch.test=Probar instalación
version.switch=Cambiar Instancia
version.launch_script=Exportar script de ejecución
@@ -1456,6 +1466,8 @@ version.manage.remove_libraries=Borrar todas las bibliotecas
version.manage.rename=Renombrar instancia
version.manage.rename.message=Introduzca el nombre de la nueva instancia
version.manage.rename.fail=No se ha podido renombrar la instancia, algunos archivos pueden estar en uso o el nombre contiene un carácter no válido.
version.search=Nombre
version.search.prompt=Introduzca el nombre de la versión para buscar.
version.settings=Configuración
version.update=Actualizar Modpack
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=Оказал большую технич
about.thanks_to.zkitefly.statement=Отвечает за поддержание документации HMCL.
about.thanks_to.mcbbs=MCBBS (Китайский форум Minecraft)
about.thanks_to.mcbbs.statement=За предоставление зеркала загрузки mcbbs.net для пользователей материкового Китая. (Больше недоступно)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=За предоставление услуги ускорения кэширования информации о модах для пользователей из материкового Китая.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=За предоставление упрощенного китайского перевода и вики для различных модов.
about.thanks_to.red_lnn.statement=За предоставление фонового изображения.
@@ -715,7 +717,7 @@ install.installer.external_version=%s (Устанавливается внешн
install.installing=Установка
install.modpack=Установить модпак
install.modpack.installation=Установка модпак
install.name.invalid=Имя содержит символы, отличные от ASCII (например, эмодзи или символы CJK).\nРекомендуется изменить имя, включив в него только английские буквы, цифры и подчеркивание, чтобы избежать возможных проблем при запуске игры.\nВы хотите продолжить процесс?
install.name.invalid=Имя содержит специальные символы (например, эмодзи или символы CJK).\nРекомендуется изменить имя, включив в него только английские буквы, цифры и подчеркивание, чтобы избежать возможных проблем при запуске игры.\nВы хотите продолжить процесс?
install.new_game=Установить сборку
install.new_game.already_exists=Это имя сборки уже существует. Пожалуйста, используйте другое имя.
install.new_game.current_game_version=Текущая версия сборки
@@ -829,6 +831,8 @@ launcher.crash.java_internal_error=Лаунчер столкнулся с фат
launcher.crash.hmcl_out_dated=Лаунчер столкнулся с фатальной ошибкой! Ваш лаунчер устарел. Обновите его!
launcher.update_java=Пожалуйста, обновите версию Java.
libraries.download=Скачивание библиотек
login.empty_username=Имя пользователя не установлено!
login.enter_password=Введите пароль.
@@ -1426,12 +1430,18 @@ version.empty=Нет сборок
version.empty.add=Установить новую сборок
version.empty.launch=Нет доступных сборок. Нажав «ОК», вы перейдете на страницу «Скачать»\n\nВы также можете скачать игру или переключить директорию игры с помощью кнопок «Скачать» или «Все сборок» на главной странице HMCL.
version.empty.hint=Нет сборок.\nВы можете попробовать перейти в другую папку с игрой или нажать здесь, чтобы скачать игру.
version.game.all=Все
version.game.april_fools=День смеха
version.game.old=Исторические
version.game.release=Релиз
version.game.releases=Релизы
version.game.snapshot=Снапшот
version.game.snapshots=Снапшоты
version.game.type=Тип
version.launch=Играть
version.launch.empty=Играть
version.launch.empty.installing=Установка игры
version.launch.empty.tooltip=Установить и запустить последнюю официальную версию игры.
version.launch.test=Тестовая игра
version.switch=Сменить сборку
version.launch_script=Создать сценарий запуска
@@ -1456,6 +1466,8 @@ version.manage.remove_libraries=Удалить файлы библиотек
version.manage.rename=Переименовать
version.manage.rename.message=Введите новое название для этой сборки
version.manage.rename.fail=Не удалось переименовать сборку. Возможно, некоторые файлы уже используются или в их имени содержится недопустимый символ.
version.search=Название
version.search.prompt=Введите название версии для поиска
version.settings=Настройки
version.update=Обновить модпак
@@ -39,7 +39,7 @@ about.thanks_to.zkitefly.statement=負責維護 HMCL 的文件
about.thanks_to.mcbbs=MCBBS (我的世界中文論壇)
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 (現已停止服務)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=提供 Mod 信息緩存加速服務
about.thanks_to.mcim.statement=為中國大陸使用者提供模組資訊快取加速服務
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
about.thanks_to.mcmod.statement=提供模組簡體中文名映射表與模組百科
about.thanks_to.red_lnn.statement=提供預設背景圖
@@ -643,6 +643,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher 由於目前 Java
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
launcher.update_java=請更新你的 Java
libraries.download=下載依賴庫
login.empty_username=你還未設定使用者名稱!
login.enter_password=請輸入你的密碼
@@ -39,7 +39,7 @@ about.thanks_to.zkitefly.statement=负责维护 HMCL 的文档
about.thanks_to.mcbbs=MCBBS (我的世界中文论坛)
about.thanks_to.mcbbs.statement=提供 MCBBS 下载源 (现已停止服务)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=提供 Mod 信息缓存加速服务
about.thanks_to.mcim.statement=为中国大陆用户提供模组信息缓存加速服务
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
about.thanks_to.mcmod.statement=提供模组简体中文名映射表与模组百科
about.thanks_to.red_lnn.statement=提供默认背景图
@@ -653,6 +653,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher 由于当前 Java
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
libraries.download=下载依赖库
login.empty_username=你还未设置用户名!
login.enter_password=请输入你的密码
@@ -48,6 +48,7 @@ public class DefaultGameBuilder extends GameBuilder {
Task<Version> libraryTask = Task.supplyAsync(() -> new Version(name));
libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, "game", gameVersion));
stages.add("hmcl.install.game:" + gameVersion);
stages.add("hmcl.install.libraries");
stages.add("hmcl.install.assets");
for (Map.Entry<String, String> entry : toolVersions.entrySet()) {
@@ -75,6 +75,7 @@ public final class GameLibrariesTask extends Task<Void> {
this.integrityCheck = integrityCheck;
this.libraries = libraries;
setStage("hmcl.install.libraries");
setSignificance(TaskSignificance.MODERATE);
}
@@ -115,6 +116,7 @@ public final class GameLibrariesTask extends Task<Void> {
@Override
public void execute() throws IOException {
int progress = 0;
GameRepository gameRepository = dependencyManager.getGameRepository();
for (Library library : libraries) {
if (!library.appliesToCurrentEnvironment()) {
@@ -135,10 +137,17 @@ public final class GameLibrariesTask extends Task<Void> {
}
}
if (shouldDownloadLibrary(gameRepository, version, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))) {
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library).withCounter("hmcl.install.libraries"));
} else {
dependencyManager.getCacheRepository().tryCacheLibrary(library, file.toPath());
}
updateProgress(++progress, libraries.size());
}
if (!dependencies.isEmpty()) {
getProperties().put("total", dependencies.size());
notifyPropertiesChanged();
}
}
@@ -45,7 +45,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
private static final String PREFIX = "https://api.curseforge.com";
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getManifestAttribute("CurseForge-Api-Key", ""));
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
@@ -24,7 +24,6 @@ import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.ReflectionHelper;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
@@ -35,10 +34,9 @@ import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -1009,8 +1007,19 @@ public abstract class Task<T> {
void execute(T result, Exception exception) throws Exception;
}
private static final String PACKAGE_PREFIX = Task.class.getPackageName() + ".";
private static final Predicate<StackWalker.StackFrame> PREDICATE = stackFrame -> !stackFrame.getClassName().startsWith(PACKAGE_PREFIX);
private static final Function<Stream<StackWalker.StackFrame>, Optional<StackWalker.StackFrame>> FUNCTION = stream -> stream.filter(PREDICATE).findFirst();
private static final Function<StackWalker.StackFrame, String> FRAME_MAPPING = frame -> {
String fileName = frame.getFileName();
if (fileName != null)
return frame.getClassName() + '.' + frame.getMethodName() + '(' + fileName + ':' + frame.getLineNumber() + ')';
else
return frame.getClassName() + '.' + frame.getMethodName();
};
private static String getCaller() {
return ReflectionHelper.getCaller(packageName -> !"org.jackhuang.hmcl.task".equals(packageName)).toString();
return StackWalker.getInstance().walk(FUNCTION).map(FRAME_MAPPING).orElse("Unknown");
}
private static final class SimpleTask<T> extends Task<T> {
@@ -1,49 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.util.function.Predicate;
/**
*
* @author huangyuhui
*/
public final class ReflectionHelper {
private ReflectionHelper() {
}
/**
* Get caller, this method is caller sensitive.
*
* @param packageFilter returns false if we consider the given package is internal calls, not the caller
* @return the caller, method name, source file, line number
*/
public static StackTraceElement getCaller(Predicate<String> packageFilter) {
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
// element[0] is Thread.currentThread().getStackTrace()
// element[1] is ReflectionHelper.getCaller(packageFilter)
// so element[2] is caller of this method.
StackTraceElement caller = elements[2];
for (int i = 3; i < elements.length; ++i) {
if (packageFilter.test(StringUtils.substringBeforeLast(elements[i].getClassName(), '.')) &&
!caller.getClassName().equals(elements[i].getClassName()))
return elements[i];
}
return caller;
}
}
@@ -20,47 +20,51 @@ package org.jackhuang.hmcl.util.io;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.Properties;
public final class JarUtils {
private JarUtils() {
}
private static final Path THIS_JAR;
private static final Manifest manifest;
private static final Properties properties = new Properties();
static {
CodeSource cs = JarUtils.class.getProtectionDomain().getCodeSource();
Class<?> entryPointClass = null;
CodeSource cs = null;
try {
entryPointClass = Class.forName("org.jackhuang.hmcl.EntryPoint");
cs = entryPointClass.getProtectionDomain().getCodeSource();
} catch (ClassNotFoundException ignored) {
}
if (cs == null) {
THIS_JAR = null;
manifest = new Manifest();
} else {
Path path;
try {
path = Paths.get(cs.getLocation().toURI()).toAbsolutePath();
path = Path.of(cs.getLocation().toURI()).toAbsolutePath();
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
path = null;
}
if (path == null || !Files.isRegularFile(path)) {
THIS_JAR = null;
manifest = new Manifest();
} else {
THIS_JAR = path;
Manifest mn;
try (JarFile file = new JarFile(path.toFile())) {
mn = file.getManifest();
} catch (IOException e) {
mn = new Manifest();
THIS_JAR = path != null && Files.isRegularFile(path) ? path : null;
}
if (entryPointClass != null) {
InputStream input = entryPointClass.getResourceAsStream("/assets/hmcl.properties");
if (input != null) {
try (var reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
properties.load(reader);
} catch (IOException ignored) {
}
manifest = mn;
}
}
}
@@ -70,8 +74,7 @@ public final class JarUtils {
return THIS_JAR;
}
public static String getManifestAttribute(String name, String defaultValue) {
String value = manifest.getMainAttributes().getValue(name);
return value != null ? value : defaultValue;
public static String getAttribute(String name, String defaultValue) {
return properties.getProperty(name, defaultValue);
}
}
@@ -186,8 +186,8 @@ public final class NetworkUtils {
}
}
// Invalid surrogate pair, encode as '?'
builder.append("%3F");
// Invalid surrogate pair, encode as U+FFFD (replacement character)
encodeCodePoint(builder, 0xfffd);
continue;
}
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
@@ -93,6 +94,9 @@ final class WindowsGPUDetector {
return null;
} else if (object instanceof String[]) {
return String.join(" ", (String[]) object);
} else if (object instanceof byte[]) {
return new String((byte[]) object, StandardCharsets.UTF_16LE)
.replace("\0", "");
} else {
return object.toString();
}
@@ -98,12 +98,20 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
this.value = value;
}
public boolean isSpecial() {
return this instanceof Special;
public boolean isAprilFools() {
if (this instanceof Special)
return true;
if (this instanceof Snapshot) {
Snapshot snapshot = (Snapshot) this;
return snapshot.intValue == Snapshot.toInt(15, 14, 'a');
}
return false;
}
enum Type {
PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW
PRE_CLASSIC, CLASSIC, INDEV, INFDEV, ALPHA, BETA, NEW
}
abstract Type getType();
@@ -141,11 +149,15 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
prefixLength = "rd-".length();
break;
case 'i':
if (!value.startsWith("inf-")) {
if (value.startsWith("inf-")) {
type = Type.INFDEV;
prefixLength = "inf-".length();
} else if (value.startsWith("in-")) {
type = Type.INDEV;
prefixLength = "in-".length();
} else {
throw new IllegalArgumentException(value);
}
type = Type.INFDEV;
prefixLength = "inf-".length();
break;
case 'a':
type = Type.ALPHA;
@@ -18,17 +18,14 @@
package org.jackhuang.hmcl.util.versioning;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.Objects;
import java.util.*;
/**
* Copied from org.apache.maven.artifact.versioning.ComparableVersion
* Apache License 2.0
*
* <p>
* Maybe we can migrate to org.jenkins-ci:version-number:1.7?
*
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
*/
public final class VersionNumber implements Comparable<VersionNumber> {
@@ -220,9 +217,17 @@ public final class VersionNumber implements Comparable<VersionNumber> {
*/
private static final class StringItem implements Item {
private final String value;
private final boolean pre;
StringItem(String value) {
this.value = value;
String lower = value.trim().toLowerCase(Locale.ROOT);
this.pre = lower.startsWith("alpha")
|| lower.startsWith("beta")
|| lower.startsWith("pre")
|| lower.startsWith("rc")
|| lower.startsWith("experimental");
}
public int getType() {
@@ -235,8 +240,8 @@ public final class VersionNumber implements Comparable<VersionNumber> {
public int compareTo(Item item) {
if (item == null) {
// 1-string > 1
return 1;
// 1-beta < 1 < 1-string
return pre ? -1 : 1;
}
switch (item.getType()) {
case LONG_ITEM:
@@ -45,10 +45,10 @@ public class NetworkUtilsTest {
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95?a=10+20", encodeLocation("https://www.example.com/测试?a=10 20"));
// Invalid surrogate pair
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uD83D"));
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uDE07"));
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uD83Dtest"));
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uDE07test"));
assertEquals("https://www.example.com/%EF%BF%BD", encodeLocation("https://www.example.com/\uD83D"));
assertEquals("https://www.example.com/%EF%BF%BD", encodeLocation("https://www.example.com/\uDE07"));
assertEquals("https://www.example.com/%EF%BF%BDtest", encodeLocation("https://www.example.com/\uD83Dtest"));
assertEquals("https://www.example.com/%EF%BF%BDtest", encodeLocation("https://www.example.com/\uDE07test"));
}
@Test
@@ -99,9 +99,34 @@ public final class GameVersionNumberTest {
assertEquals(VersionNumber.asVersion(versionNumber), old.versionNumber);
}
private static boolean isAprilFools(String version) {
return GameVersionNumber.asGameVersion(version).isAprilFools();
}
@Test
public void testIsAprilFools() {
assertTrue(isAprilFools("15w14a"));
assertTrue(isAprilFools("1.RV-Pre1"));
assertTrue(isAprilFools("3D Shareware v1.34"));
assertTrue(isAprilFools("2.0"));
assertTrue(isAprilFools("20w14infinite"));
assertTrue(isAprilFools("22w13oneBlockAtATime"));
assertTrue(isAprilFools("23w13a_or_b"));
assertTrue(isAprilFools("24w14potato"));
assertTrue(isAprilFools("25w14craftmine"));
assertFalse(isAprilFools("1.21.8"));
assertFalse(isAprilFools("1.21.8-rc1"));
assertFalse(isAprilFools("25w21a"));
assertFalse(isAprilFools("13w12~"));
assertFalse(isAprilFools("15w14b"));
}
@Test
public void testParseOld() {
assertOldVersion("rd-132211", GameVersionNumber.Type.PRE_CLASSIC, "132211");
assertOldVersion("in-20100223", GameVersionNumber.Type.INDEV, "20100223");
assertOldVersion("in-20100212-2", GameVersionNumber.Type.INDEV, "20100212-2");
assertOldVersion("inf-20100618", GameVersionNumber.Type.INFDEV, "20100618");
assertOldVersion("inf-20100330-1", GameVersionNumber.Type.INFDEV, "20100330-1");
assertOldVersion("a1.0.6", GameVersionNumber.Type.ALPHA, "1.0.6");
@@ -90,12 +90,17 @@ public class VersionNumberTest {
assertLessThan("1.99999999999999999999", "1.199999999999999999999");
assertLessThan("1.99999999999999999999", "2");
assertLessThan("1.99999999999999999999", "2.0");
assertLessThan("1.0", "1.0-zzz");
assertLessThan("1.0-beta.1", "1.0");
assertLessThan("1.0-alpha.1", "1.0-beta.1");
assertLessThan("3.6.15", "3.6.15.289");
assertLessThan("3.6.15.289", "3.6.16");
}
@Test
public void testSorting() {
final Comparator<String> comparator = ((Comparator<String>) VersionNumber::compare).thenComparing(String::compareTo);
final List<String> input = Collections.unmodifiableList(Arrays.asList(
final List<String> input = List.of(
"0",
"0.10.0",
"1.6.4",
@@ -120,6 +125,9 @@ public class VersionNumberTest {
"1.8.9",
"1.8.9-forge1902",
"1.9",
"1.10-alpha.2",
"1.10-beta.1",
"1.10-beta.2",
"1.10",
"1.10.2",
"1.10.2-AOE",
@@ -140,7 +148,8 @@ public class VersionNumberTest {
"1.99999999999999999999",
"2",
"2.0",
"2.1"));
"2.1"
);
List<String> output = new ArrayList<>(input);
output.sort(comparator);
+4 -4
View File
@@ -4,13 +4,13 @@
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.7) |
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (Old World) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
+4 -4
View File
@@ -4,13 +4,13 @@
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.7) |
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
+4 -4
View File
@@ -4,13 +4,13 @@
| | Windows | Linux | macOS | FreeBSD |
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.7) |
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
| S390x | / | ❔ | / | / |
+1 -1
View File
@@ -52,7 +52,7 @@ Simply execute the following command in the project root directory:
./gradlew clean build
```
Make sure you have Java 11 or later installed.
Make sure you have Java 17 or later installed.
## JVM Options (for debugging)
+1 -1
View File
@@ -52,7 +52,7 @@ HMCL 有着强大的跨平台能力。它不仅支持 Windows、Linux、macOS
./gradlew clean build
```
请确保你至少安装了 JDK 11 或更高版本。
请确保你至少安装了 JDK 17 或更高版本。
## JVM 选项 (用于调试)
+1 -1
View File
@@ -52,7 +52,7 @@ HMCL 有著強大的跨平臺能力。它不僅支援 Windows、Linux、macOS
./gradlew clean build
```
請確保你至少安裝了 JDK 11 或更高版本。
請確保你至少安裝了 JDK 17 或更高版本。
## JVM 選項 (用於除錯)