From 94f1f3ba1db5d97e59ba86ed6483b30bb3587c0b Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 12 Sep 2026 19:13:40 +0800 Subject: [PATCH 01/12] impl --- .../jackhuang/hmcl/game/LauncherHelper.java | 81 ++++--- .../org/jackhuang/hmcl/ui/Controllers.java | 8 + .../java/org/jackhuang/hmcl/ui/LogWindow.java | 39 +++- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 2 + .../hmcl/ui/instances/GameProcessPage.java | 217 ++++++++++++++++++ .../org/jackhuang/hmcl/ui/main/RootPage.java | 10 +- .../resources/assets/lang/I18N.properties | 4 + .../resources/assets/lang/I18N_ar.properties | 4 + .../resources/assets/lang/I18N_de.properties | 4 + .../resources/assets/lang/I18N_es.properties | 4 + .../resources/assets/lang/I18N_ja.properties | 4 + .../resources/assets/lang/I18N_lzh.properties | 4 + .../resources/assets/lang/I18N_ru.properties | 4 + .../resources/assets/lang/I18N_uk.properties | 4 + .../assets/lang/I18N_zh_Hans.properties | 4 + .../assets/lang/I18N_zh_Hant.properties | 4 + 16 files changed, 353 insertions(+), 44 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index aaef6607bce..4ede0690add 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -18,6 +18,9 @@ package org.jackhuang.hmcl.game; import com.jfoenix.controls.JFXButton; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.collections.ObservableList; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.*; @@ -43,10 +46,12 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; +import org.jackhuang.hmcl.ui.instances.GameProcessPage; import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.ResponseCodeException; +import org.jackhuang.hmcl.util.logging.Logger; import org.jackhuang.hmcl.util.platform.*; import org.jackhuang.hmcl.util.platform.windows.WinReg; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -290,7 +295,7 @@ private void launch0() { launchOptions, launcherVisibility == LauncherVisibility.CLOSE ? null // Unnecessary to start listening to game process output when close launcher immediately after game launched. - : new HMCLProcessListener(authInfo, launchOptions, launchingLatch, gameInstance.getVersion().compareTo(GameVersionNumber.unknown()) != 0) + : new HMCLProcessListener(this, authInfo, launchOptions, launchingLatch, gameInstance.getVersion().compareTo(GameVersionNumber.unknown()) != 0) ); }).thenComposeAsync(launcher -> { // launcher is prev task's result if (scriptFile == null) { @@ -834,9 +839,10 @@ private void enableAutoAgentForCurrentSetting() { /// The managed process listener. /// Guarantee that one Java [Process], one [HMCLProcessListener]. /// Because every time we launched a game, we generates a new [HMCLProcessListener] - private final class HMCLProcessListener implements ProcessListener { + public static final class HMCLProcessListener implements ProcessListener { private final ReentrantLock lock = new ReentrantLock(); + private final LauncherHelper launcherHelper; private final LaunchOptions launchOptions; private ManagedProcess process; private volatile boolean lwjgl; @@ -848,7 +854,10 @@ private final class HMCLProcessListener implements ProcessListener { private Thread submitLogThread; private LinkedBlockingQueue logBuffer; - public HMCLProcessListener(AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) { + private final ReadOnlyBooleanWrapper exited = new ReadOnlyBooleanWrapper(false); + + public HMCLProcessListener(LauncherHelper launcherHelper, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) { + this.launcherHelper = launcherHelper; this.launchOptions = launchOptions; this.launchingLatch = launchingLatch; this.detectWindow = detectWindow; @@ -856,6 +865,22 @@ public HMCLProcessListener(AuthInfo authInfo, LaunchOptions launchOptions, Count this.logs = new CircularArrayList<>(Log.getLogLines() + 1); } + public ManagedProcess getProcess() { + return process; + } + + public LogWindow getLogWindow() { + return logWindow; + } + + public HMCLGameInstance getGameInstance() { + return launcherHelper.gameInstance; + } + + public ReadOnlyBooleanProperty exitedProperty() { + return exited.getReadOnlyProperty(); + } + @Override public void setProcess(ManagedProcess process) { this.process = process; @@ -869,11 +894,14 @@ public void setProcess(ManagedProcess process) { LOG.info("Process ClassPath: " + classpath); } - if (showLogs) { + { CountDownLatch logWindowLatch = new CountDownLatch(1); runLater(() -> { logWindow = new LogWindow(process, logs); - logWindow.show(); + logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(process.getCommands())), Log4jLevel.INFO)); + if (process.getClasspath() != null) + logWindow.logLine(new Log("ClassPath: " + process.getClasspath(), Log4jLevel.INFO)); + if (launcherHelper.showLogs) logWindow.show(); logWindowLatch.countDown(); }); @@ -923,10 +951,12 @@ public void run() { Thread.currentThread().interrupt(); } } + + GameProcessPage.addProcessListener(this); } private void finishLaunch() { - switch (launcherVisibility) { + switch (launcherHelper.launcherVisibility) { case HIDE_AND_REOPEN: runLater(() -> { // If application was stopped and execution services did not finish termination, @@ -973,20 +1003,9 @@ public void onLog(String log, boolean isErrorStream) { log = log.replace(forbiddenAccessToken, ""); Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null; - if (showLogs) { - if (level == null) - level = Objects.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO); - logBuffer.add(new Log(log, level)); - } else { - lock.lock(); - try { - logs.addLast(new Log(log, level)); - if (logs.size() > Log.getLogLines()) - logs.removeFirst(); - } finally { - lock.unlock(); - } - } + if (level == null) + level = Objects.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO); + logBuffer.add(new Log(log, level)); if (!lwjgl) { String lowerCaseLog = log.toLowerCase(Locale.ROOT); @@ -1006,16 +1025,16 @@ public void onLog(String log, boolean isErrorStream) { @Override public void onExit(int exitCode, ExitType exitType) { - if (showLogs) { - logBuffer.add(new Log(String.format("[%s] [HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", TIME_FORMATTER.format(Instant.now()), exitCode, exitCode, exitType), Log4jLevel.INFO)); - submitLogThread.interrupt(); - try { - submitLogThread.join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + logBuffer.add(new Log(String.format("[%s] [HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", TIME_FORMATTER.format(Instant.now()), exitCode, exitCode, exitType), Log4jLevel.INFO)); + submitLogThread.interrupt(); + try { + submitLogThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } + runInFX(() -> exited.set(true)); + launchingLatch.countDown(); if (exitType == ExitType.INTERRUPTED) @@ -1033,11 +1052,11 @@ public void onExit(int exitCode, ExitType exitType) { } if (exitType != ExitType.NORMAL) { - gameInstance.markLaunchedAbnormally(); - runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show()); + launcherHelper.gameInstance.markLaunchedAbnormally(); + runLater(() -> new GameCrashWindow(process, exitType, launcherHelper.gameInstance, launchOptions, logs).show()); } - checkExit(); + launcherHelper.checkExit(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 52411fb21bd..2504d993263 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -51,6 +51,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.decorator.Decorator; import org.jackhuang.hmcl.ui.download.DownloadPage; +import org.jackhuang.hmcl.ui.instances.GameProcessPage; import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; import org.jackhuang.hmcl.ui.terracotta.TerracottaPage; @@ -106,6 +107,7 @@ public final class Controllers { }); private static LauncherSettingsPage settingsPage; private static Lazy terracottaPage = new Lazy<>(TerracottaPage::new); + private static Lazy gameProcessPage = new Lazy<>(GameProcessPage::new); private Controllers() { } @@ -195,6 +197,11 @@ public static Node getTerracottaPage() { return terracottaPage.get(); } + @FXThread + public static Node getGameProcessPage() { + return gameProcessPage.get(); + } + /// Returns the initialized main-window decorator. /// /// @return the application-wide main-window decorator @@ -638,6 +645,7 @@ public static void shutdown() { accountListPage = null; settingsPage = null; terracottaPage = null; + gameProcessPage = null; decorator = null; FXUtils.shutdown(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index 6ac9d2daf2b..da4c022f336 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -44,6 +44,8 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.platform.ManagedProcess; import org.jackhuang.hmcl.util.platform.SystemUtils; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.io.IOException; import java.nio.file.Files; @@ -79,13 +81,17 @@ public final class LogWindow extends Stage { } private final LogWindowImpl impl; - private final ManagedProcess gameProcess; + private final @Nullable ManagedProcess gameProcess; + + public LogWindow() { + this(null, new CircularArrayList<>()); + } public LogWindow(ManagedProcess gameProcess) { this(gameProcess, new CircularArrayList<>()); } - public LogWindow(ManagedProcess gameProcess, CircularArrayList logs) { + public LogWindow(@Nullable ManagedProcess gameProcess, CircularArrayList logs) { Themes.applyNativeDarkMode(this); this.logs = logs; @@ -101,7 +107,7 @@ public LogWindow(ManagedProcess gameProcess, CircularArrayList logs) { this.gameProcess = gameProcess; - FXUtils.addMacOSCloseWindowHandler(this, () -> !gameProcess.isRunning()); + FXUtils.addMacOSCloseWindowHandler(this, () -> gameProcess == null || !gameProcess.isRunning()); } public void logLine(Log log) { @@ -130,6 +136,10 @@ public void logLines(List logs) { autoScroll(); } + public @Unmodifiable ObservableList getLogs() { + return FXCollections.unmodifiableObservableList(impl.listView.getItems()); + } + private void shakeLogs() { impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList())); autoScroll(); @@ -198,7 +208,7 @@ private final class LogWindowImpl extends Control { } private void onTerminateGame() { - LogWindow.this.gameProcess.stop(); + if (gameProcess != null) gameProcess.stop(); } private void onClear() { @@ -234,7 +244,7 @@ private void onExportDump(SpinnerPane pane) { Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath(); try { - if (gameProcess.isRunning()) { + if (gameProcess != null && gameProcess.isRunning()) { GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile); FXUtils.showFileInExplorer(dumpFile); } @@ -251,7 +261,7 @@ private void onExportDump(SpinnerPane pane) { }); } - private ManagedProcess getGameProcess() { + private @Nullable ManagedProcess getGameProcess() { return gameProcess; } @@ -433,12 +443,17 @@ protected void updateItem(Log item, boolean empty) { clearButton.setOnAction(e -> getSkinnable().onClear()); hBox.getChildren().setAll(autoScrollCheckBox, wrapTextCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton); - control.getGameProcess().getProcess() - .onExit() - .thenRunAsync(() -> { - terminateButton.setDisable(true); - exportDumpButton.setDisable(true); - }, Schedulers.javafx()); + if (control.getGameProcess() != null) { + control.getGameProcess().getProcess() + .onExit() + .thenRunAsync(() -> { + terminateButton.setDisable(true); + exportDumpButton.setDisable(true); + }, Schedulers.javafx()); + } else { + terminateButton.setDisable(true); + exportDumpButton.setDisable(true); + } vbox.getChildren().add(bottom); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 9991ac13b85..79948c8bb33 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -114,10 +114,12 @@ public enum SVG { SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons + SHUTDOWN("M12 22q-2.075 0 -3.9 -0.7875T4.925 19.075q-1.35 -1.35 -2.1375 -3.175T2 12q0 -2.1 0.7875 -3.9125T4.925 4.925l1.4 1.4q-1.1 1.1 -1.7125 2.55T4 12q0 3.35 2.325 5.675t5.675 2.325q3.35 0 5.675 -2.325t2.325 -5.675q0 -1.675 -0.6125 -3.125T17.675 6.325l1.4 -1.4q1.35 1.35 2.1375 3.1625T22 12q0 2.075 -0.7875 3.9T19.075 19.075q-1.35 1.35 -3.175 2.1375T12 22zm-1 -9l0 -11l2 0l0 11l-2 0z"), STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"), STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"), STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"), STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"), + TERMINAL("M4 20q-0.825 0 -1.4125 -0.5875T2 18l0 -12q0 -0.825 0.5875 -1.4125T4 4l16 0q0.825 0 1.4125 0.5875T22 6l0 12q0 0.825 -0.5875 1.4125T20 20L4 20zm0 -2l16 0l0 -10L4 8l0 10zm3.5 -1 -1.4 -1.4 2.575 -2.6 -2.6 -2.6 1.425 -1.4 4 4 -4 4zm4.5 0l0 -2l6 0l0 2L12 17z"), TEXTURE("M4.4 21q-.475-.1-.8875-.5125T3 19.6L19.6 3q.525.125.9.5125t.525.8875L4.4 21ZM3 14.7v-2.8L11.9 3h2.8L3 14.7ZM3 7V5q0-.825.5875-1.4125T5 3h2L3 7Zm14 14 4-4v2q0 .825-.5875 1.4125T19 21h-2Zm-7.7 0L21 9.3v2.8L12.1 21H9.3Z"), TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"), TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java new file mode 100644 index 00000000000..38c62ac135d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -0,0 +1,217 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXListView; +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ListCell; +import javafx.scene.control.Skin; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.game.LauncherHelper; +import org.jackhuang.hmcl.game.Log; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.MDListCell; +import org.jackhuang.hmcl.ui.construct.PageAware; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.FXThread; + +import java.lang.ref.WeakReference; +import java.util.*; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class GameProcessPage extends ListPageBase implements DecoratorPage, PageAware { + + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("game.process"))); + + @FXThread + private static final Map idToLaunchedCount = new HashMap<>(); + + @FXThread + private static final List processHolders = new ArrayList<>(); + + @FXThread + private static void cleanupProcessListeners() { + processHolders.removeIf(holder -> holder.exited.get()); + } + + public static void addProcessListener(LauncherHelper.HMCLProcessListener processListener) { + FXUtils.runInFX(() -> processHolders.add(new GameProcessHolder(processListener))); + } + + public GameProcessPage() { + + } + + @Override + protected Skin createDefaultSkin() { + return new GameProcessPageSkin(this); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } + + @Override + public void refresh() { + setLoading(true); + cleanupProcessListeners(); + getItems().setAll(processHolders); + setLoading(false); + } + + @Override + public void onPageShown() { + refresh(); + } + + @Override + public void onPageHidden() { + getItems().clear(); + } + + public static final class GameProcessHolder { + + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final WeakReference listenerRef; + + @SuppressWarnings("FieldCanBeLocal") + private final WeakListenerHolder holder = new WeakListenerHolder(); + + private final String title; + + private final ObservableList logs = FXCollections.observableArrayList(); + private final StringProperty lastLogLine = new SimpleStringProperty(""); + private final BooleanProperty exited = new SimpleBooleanProperty(); + + private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { + this.listenerRef = new WeakReference<>(processListener); + { + String id = processListener.getGameInstance().getId().id(); + int i = idToLaunchedCount.computeIfAbsent(id, k -> 0) + 1; + idToLaunchedCount.put(id, i); + this.title = id + " #" + i; + } + { + Bindings.bindContent(logs, processListener.getLogWindow().getLogs()); + if (!logs.isEmpty()) { + lastLogLine.set(logs.get(logs.size() - 1).getLog()); + } + logs.addListener((InvalidationListener) o -> { + if (!logs.isEmpty()) { + lastLogLine.set(logs.get(logs.size() - 1).getLog()); + } else { + lastLogLine.set(""); + } + }); + } + holder.onWeakChangeAndOperate(processListener.exitedProperty(), b -> { + if (b) { + var currentLogs = processListener.getLogWindow().getLogs(); + this.lastLogLine.set(currentLogs.get(currentLogs.size() - 1).getLog()); // I don't know why but this is necessary + this.exited.set(true); + } + }); + } + } + + private static final class GameProcessPageSkin extends ToolbarListPageSkin { + + public GameProcessPageSkin(GameProcessPage skinnable) { + super(skinnable); + } + + @Override + protected List initializeToolbar(GameProcessPage skinnable) { + return List.of( + ToolbarListPageSkin.createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) + ); + } + + @Override + protected ListCell createListCell(JFXListView listView) { + return new GameProcessCell(listView); + } + } + + private static final class GameProcessCell extends MDListCell { + + private final TwoLineListItem content = new TwoLineListItem(); + private final JFXButton logWindowButton = FXUtils.newToggleButton4(SVG.TERMINAL); + private final JFXButton terminateButton = FXUtils.newToggleButton4(SVG.SHUTDOWN); + + public GameProcessCell(JFXListView listView) { + super(listView); + + HBox container = new HBox(8); + container.setPickOnBounds(false); + container.setAlignment(Pos.CENTER_LEFT); + HBox.setHgrow(content, Priority.ALWAYS); + content.setMouseTransparent(true); + + FXUtils.installFastTooltip(logWindowButton, i18n("game.process.show_log")); + FXUtils.installFastTooltip(terminateButton, i18n("game.process.terminate")); + + container.getChildren().setAll(content, logWindowButton, terminateButton); + StackPane.setMargin(container, new Insets(8)); + getContainer().getChildren().setAll(container); + } + + @Override + protected void updateControl(GameProcessHolder item, boolean empty) { + if (item == null || empty) { + logWindowButton.setOnAction(null); + terminateButton.setOnAction(null); + terminateButton.disableProperty().unbind(); + return; + } + + content.setTitle(item.title); + content.subtitleProperty().bind(item.lastLogLine); + + logWindowButton.setOnAction(event -> { + var listener = item.listenerRef.get(); + if (listener != null) { + listener.getLogWindow().show(); + } else { + LogWindow logWindow = new LogWindow(); + logWindow.logLines(item.logs); + logWindow.show(); + } + }); + terminateButton.setOnAction(event -> { + var listener = item.listenerRef.get(); + if (listener != null) listener.getProcess().stop(); + }); + terminateButton.disableProperty().bind(item.exited); + } + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 46662c4365f..c8df7bf19ad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -40,6 +40,7 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem; import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; +import org.jackhuang.hmcl.ui.instances.GameProcessPage; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.ui.nbt.NBTFileType; @@ -153,6 +154,12 @@ protected Skin(RootPage control) { } // fifth item in left sidebar + AdvancedListItem gameProcessItem = new AdvancedListItem(); + gameProcessItem.setLeftIcon(SVG.TERMINAL); + gameProcessItem.setTitle(i18n("game.process")); + gameProcessItem.setOnAction(e -> Controllers.navigate(Controllers.getGameProcessPage())); + + // sixth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); launcherSettingsItem.setLeftIcon(SVG.SETTINGS); launcherSettingsItem.setTitle(i18n("settings")); @@ -164,7 +171,7 @@ protected Skin(RootPage control) { FXUtils.prepareOnMouseEnter(launcherSettingsItem, Controllers::prepareSettingsPage); } - // sixth item in left sidebar + // seventh item in left sidebar AdvancedListItem terracottaItem = new AdvancedListItem(); terracottaItem.setLeftIcon(SVG.GRAPH2); terracottaItem.setTitle(i18n("terracotta")); @@ -195,6 +202,7 @@ else if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOO .add(gameListItem) .add(gameItem) .add(downloadItem) + .add(gameProcessItem) .startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT)) .add(launcherSettingsItem) .add(terracottaItem) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 4e4a2493597..901588642ed 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -732,6 +732,10 @@ game_directory.new=New Directory game_directory.use_relative_path=Use a relative path for the game directory if possible game_directory.root=Using the disk root directory as the game folder may cause the game to fail to launch and will create multiple files and folders in the disk root directory.\n\nAre you sure you want to continue? +game.process=Game Process +game.process.show_log=Show Log +game.process.terminate=Terminate + help=Help help.doc=HMCL Documentation help.detail=For data pack and modpack makers. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 5111f7490b4..14907b3d8aa 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -733,6 +733,10 @@ game_directory.new=مجلد جديد game_directory.use_relative_path=استخدام مسار نسبي لمجلد اللعبة إن أمكن # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=مساعدة help.doc=توثيق HMCL help.detail=لصانعي حزم البيانات وحزم المودات. diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 6d474be5f7b..69cc5fbc177 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -731,6 +731,10 @@ game_directory.new=Neues Verzeichnis game_directory.use_relative_path=Verwenden Sie, wenn möglich, einen relativen Pfad für das Spielverzeichnis # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=Hilfe help.doc=HMCL Dokumentation help.detail=Für Hersteller von Datenpaketen und Modpacks. diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index d61b585a426..0444f4df230 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -725,6 +725,10 @@ game_directory.new=Nuevo directorio game_directory.use_relative_path=Utilizar ruta relativa para la ruta del juego si es posible # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=Ayuda help.doc=Documentación de HMCL help.detail=Para los creadores de paquetes de datos y mods. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 323189a2bd4..04e7ada1670 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -535,6 +535,10 @@ game_directory.new=新規作成 game_directory.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=ヘルプ help.doc=ドキュメント help.detail=データパック、modpackなどのメーカー向け。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 5d23c25050f..435d0e1ad90 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -547,6 +547,10 @@ game_directory.new=另增戲案夾 game_directory.use_relative_path=苟能是,戲案夾用相對之徑 # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=助 help.doc=HMCL 之案文 help.detail=所以閲錄囊、改囊集之司南與他者 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 0474f4e690a..c7a1b8fcf41 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -717,6 +717,10 @@ game_directory.new=Новая папка game_directory.use_relative_path=По возможности используйте относительный путь для игры # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=Справка help.doc=Документация лаунчера help.detail=Для создателей набора данных и модпака. diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 4afbf45756f..39e6df7ee7e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -686,6 +686,10 @@ game_directory.new=Новий каталог game_directory.use_relative_path=Використовувати відносний шлях для каталогу гри, якщо можливо # game_directory.root= +# game.process= +# game.process.show_log= +# game.process.terminate= + help=Допомога help.doc=Документація Hello Minecraft! Лаунчера help.detail=Для творців datapack та модпаків. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties index 797ed0852b1..324490d211d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -550,6 +550,10 @@ game_directory.new=添加游戏文件夹 game_directory.use_relative_path=若可能,游戏文件夹使用相对路径 game_directory.root=使用磁盘根目录作为游戏文件夹可能会导致无法启动游戏,并会在磁盘根目录下创建多个文件(夹)。\n\n确定要继续吗? +game.process=游戏进程 +game.process.show_log=显示日志 +game.process.terminate=终止进程 + help=帮助 help.doc=HMCL 帮助文档 help.detail=可查阅数据包、整合包制作指南等内容 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties index 3c3f3527142..e058d72117f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -544,6 +544,10 @@ game_directory.new=建立新目錄 game_directory.use_relative_path=如可行,則對於遊戲目錄使用相對路徑 game_directory.root=使用磁碟根目錄作為遊戲資料夾可能會導致無法啟動遊戲,並會在磁碟根目錄下建立多個檔案和資料夾。\n\n確定要繼續嗎? +game.process=遊戲程序 +game.process.show_log=顯示日誌 +game.process.terminate=終止程序 + help=說明 help.doc=HMCL 說明文件 help.detail=可查閱資料包、模組包製作教學等內容 From 7bc4f32302597860d2958f30f4417bb7d586c97d Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 13:23:12 +0800 Subject: [PATCH 02/12] update --- .../src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 1 - .../java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java | 4 ++-- HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 4ede0690add..605e4bdc895 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -20,7 +20,6 @@ import com.jfoenix.controls.JFXButton; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; -import javafx.collections.ObservableList; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.*; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index 38c62ac135d..f2bcc10a4a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -120,8 +120,8 @@ private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { this.title = id + " #" + i; } { - Bindings.bindContent(logs, processListener.getLogWindow().getLogs()); - if (!logs.isEmpty()) { + Bindings.bindContent(logs, processListener.getLogWindow().getLogs()); + if (!logs.isEmpty()) { lastLogLine.set(logs.get(logs.size() - 1).getLog()); } logs.addListener((InvalidationListener) o -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index c8df7bf19ad..ce0865f7e88 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -40,7 +40,6 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem; import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; -import org.jackhuang.hmcl.ui.instances.GameProcessPage; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.ui.nbt.NBTFileType; From 6fef0ab2285cc974b4d11edc1518e1eb35af1350 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 13:51:59 +0800 Subject: [PATCH 03/12] update --- .../hmcl/game/GameProcessManager.java | 119 ++++++++++++++++++ .../jackhuang/hmcl/game/LauncherHelper.java | 3 +- .../java/org/jackhuang/hmcl/ui/LogWindow.java | 5 +- .../hmcl/ui/instances/GameProcessPage.java | 91 +++----------- 4 files changed, 136 insertions(+), 82 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java new file mode 100644 index 00000000000..9e576571242 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -0,0 +1,119 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game; + +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.WeakListenerHolder; +import org.jackhuang.hmcl.util.FXThread; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class GameProcessManager { + + private GameProcessManager() { + } + + @FXThread + public static final Map idToLaunchedCount = new HashMap<>(); + + @FXThread + public static final List processHolders = new ArrayList<>(); + + public static void cleanupProcessListeners() { + FXUtils.runInFX(() -> processHolders.removeIf(holder -> holder.exited.get())); + } + + public static void addProcessListener(LauncherHelper.HMCLProcessListener processListener) { + FXUtils.runInFX(() -> processHolders.add(new GameProcessHolder(processListener))); + } + + public static final class GameProcessHolder { + + private final WeakReference listenerRef; + + @SuppressWarnings("FieldCanBeLocal") + private final WeakListenerHolder holder = new WeakListenerHolder(); + + private final String id; + + private final ObservableList logs = FXCollections.observableArrayList(); + private final ReadOnlyStringWrapper lastLogLine = new ReadOnlyStringWrapper(""); + private final ReadOnlyBooleanWrapper exited = new ReadOnlyBooleanWrapper(); + + private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { + this.listenerRef = new WeakReference<>(processListener); + { + String id = processListener.getGameInstance().getId().id(); + int i = idToLaunchedCount.computeIfAbsent(id, k -> 0) + 1; + idToLaunchedCount.put(id, i); + this.id = id + " #" + i; + } + { + Bindings.bindContent(logs, processListener.getLogWindow().getLogs()); + if (!logs.isEmpty()) { + lastLogLine.set(logs.get(logs.size() - 1).getLog()); + } + logs.addListener((InvalidationListener) o -> { + if (!logs.isEmpty()) { + lastLogLine.set(logs.get(logs.size() - 1).getLog()); + } else { + lastLogLine.set(""); + } + }); + } + holder.onWeakChangeAndOperate(processListener.exitedProperty(), b -> { + if (b) { + var currentLogs = processListener.getLogWindow().getLogs(); + this.lastLogLine.set(currentLogs.get(currentLogs.size() - 1).getLog()); // I don't know why but this is necessary + processHolders.remove(this); + this.exited.set(true); + } + }); + } + + public WeakReference getListenerRef() { + return listenerRef; + } + + public String getId() { + return id; + } + + public ObservableList getLogs() { + return logs; + } + + public ReadOnlyStringProperty lastLogLineProperty() { + return lastLogLine.getReadOnlyProperty(); + } + + public ReadOnlyBooleanProperty exitedProperty() { + return exited.getReadOnlyProperty(); + } + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 605e4bdc895..fc4744dcf5a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -45,7 +45,6 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; -import org.jackhuang.hmcl.ui.instances.GameProcessPage; import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; @@ -951,7 +950,7 @@ public void run() { } } - GameProcessPage.addProcessListener(this); + GameProcessManager.addProcessListener(this); } private void finishLaunch() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index da4c022f336..f9a59782d83 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -45,7 +45,6 @@ import org.jackhuang.hmcl.util.platform.ManagedProcess; import org.jackhuang.hmcl.util.platform.SystemUtils; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.Unmodifiable; import java.io.IOException; import java.nio.file.Files; @@ -136,8 +135,8 @@ public void logLines(List logs) { autoScroll(); } - public @Unmodifiable ObservableList getLogs() { - return FXCollections.unmodifiableObservableList(impl.listView.getItems()); + public ObservableList getLogs() { + return impl.listView.getItems(); } private void shakeLogs() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index f2bcc10a4a5..7bbc9f41971 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -19,11 +19,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXListView; -import javafx.beans.InvalidationListener; -import javafx.beans.binding.Bindings; import javafx.beans.property.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -32,39 +28,25 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; -import org.jackhuang.hmcl.game.LauncherHelper; -import org.jackhuang.hmcl.game.Log; +import org.jackhuang.hmcl.game.GameProcessManager; +import org.jackhuang.hmcl.game.GameProcessManager.GameProcessHolder; import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.MDListCell; import org.jackhuang.hmcl.ui.construct.PageAware; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.FXThread; -import java.lang.ref.WeakReference; import java.util.*; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class GameProcessPage extends ListPageBase implements DecoratorPage, PageAware { +/// Page for managing running game processes. +/// +/// @author Calboot +public class GameProcessPage extends ListPageBase implements DecoratorPage, PageAware { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("game.process"))); - @FXThread - private static final Map idToLaunchedCount = new HashMap<>(); - - @FXThread - private static final List processHolders = new ArrayList<>(); - - @FXThread - private static void cleanupProcessListeners() { - processHolders.removeIf(holder -> holder.exited.get()); - } - - public static void addProcessListener(LauncherHelper.HMCLProcessListener processListener) { - FXUtils.runInFX(() -> processHolders.add(new GameProcessHolder(processListener))); - } - public GameProcessPage() { } @@ -82,8 +64,8 @@ public ReadOnlyObjectProperty stateProperty() { @Override public void refresh() { setLoading(true); - cleanupProcessListeners(); - getItems().setAll(processHolders); + GameProcessManager.cleanupProcessListeners(); + getItems().setAll(GameProcessManager.processHolders); setLoading(false); } @@ -97,51 +79,6 @@ public void onPageHidden() { getItems().clear(); } - public static final class GameProcessHolder { - - @SuppressWarnings({"unused", "FieldCanBeLocal"}) - private final WeakReference listenerRef; - - @SuppressWarnings("FieldCanBeLocal") - private final WeakListenerHolder holder = new WeakListenerHolder(); - - private final String title; - - private final ObservableList logs = FXCollections.observableArrayList(); - private final StringProperty lastLogLine = new SimpleStringProperty(""); - private final BooleanProperty exited = new SimpleBooleanProperty(); - - private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { - this.listenerRef = new WeakReference<>(processListener); - { - String id = processListener.getGameInstance().getId().id(); - int i = idToLaunchedCount.computeIfAbsent(id, k -> 0) + 1; - idToLaunchedCount.put(id, i); - this.title = id + " #" + i; - } - { - Bindings.bindContent(logs, processListener.getLogWindow().getLogs()); - if (!logs.isEmpty()) { - lastLogLine.set(logs.get(logs.size() - 1).getLog()); - } - logs.addListener((InvalidationListener) o -> { - if (!logs.isEmpty()) { - lastLogLine.set(logs.get(logs.size() - 1).getLog()); - } else { - lastLogLine.set(""); - } - }); - } - holder.onWeakChangeAndOperate(processListener.exitedProperty(), b -> { - if (b) { - var currentLogs = processListener.getLogWindow().getLogs(); - this.lastLogLine.set(currentLogs.get(currentLogs.size() - 1).getLog()); // I don't know why but this is necessary - this.exited.set(true); - } - }); - } - } - private static final class GameProcessPageSkin extends ToolbarListPageSkin { public GameProcessPageSkin(GameProcessPage skinnable) { @@ -193,24 +130,24 @@ protected void updateControl(GameProcessHolder item, boolean empty) { return; } - content.setTitle(item.title); - content.subtitleProperty().bind(item.lastLogLine); + content.setTitle(item.getId()); + content.subtitleProperty().bind(item.lastLogLineProperty()); logWindowButton.setOnAction(event -> { - var listener = item.listenerRef.get(); + var listener = item.getListenerRef().get(); if (listener != null) { listener.getLogWindow().show(); } else { LogWindow logWindow = new LogWindow(); - logWindow.logLines(item.logs); + logWindow.logLines(item.getLogs()); logWindow.show(); } }); terminateButton.setOnAction(event -> { - var listener = item.listenerRef.get(); + var listener = item.getListenerRef().get(); if (listener != null) listener.getProcess().stop(); }); - terminateButton.disableProperty().bind(item.exited); + terminateButton.disableProperty().bind(item.exitedProperty()); } } From f4920cca979b3192030396ccd2af908770ab5b71 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:13:43 +0800 Subject: [PATCH 04/12] relaunch --- .../hmcl/game/GameProcessManager.java | 75 ++++++++++++++++--- .../jackhuang/hmcl/game/LauncherHelper.java | 2 +- .../hmcl/ui/instances/GameProcessPage.java | 44 +++++------ .../resources/assets/lang/I18N.properties | 1 + .../assets/lang/I18N_zh_Hans.properties | 1 + .../assets/lang/I18N_zh_Hant.properties | 1 + 6 files changed, 90 insertions(+), 34 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java index 9e576571242..c8a0dda0042 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -23,7 +23,9 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.LogWindow; import org.jackhuang.hmcl.ui.WeakListenerHolder; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.FXThread; import java.lang.ref.WeakReference; @@ -38,17 +40,48 @@ private GameProcessManager() { } @FXThread - public static final Map idToLaunchedCount = new HashMap<>(); + private static final Map idToLaunchedCount = new HashMap<>(); @FXThread - public static final List processHolders = new ArrayList<>(); + private static final List processHolders = new ArrayList<>(); - public static void cleanupProcessListeners() { - FXUtils.runInFX(() -> processHolders.removeIf(holder -> holder.exited.get())); + @FXThread + public static final ObservableList displayedHolders = FXCollections.observableArrayList(); + + @FXThread + private static final BooleanProperty display = new SimpleBooleanProperty() { + @Override + public void invalidated() { + if (display.get()) { + updateDisplay(); + } else { + displayedHolders.clear(); + } + } + }; + + @FXThread + public static void setDisplay(boolean display) { + GameProcessManager.display.set(display); + } + + public static void updateDisplay() { + FXUtils.runInFX(() -> { + processHolders.removeIf(holder -> holder.exited.get()); + displayedHolders.setAll(processHolders); + }); + } + + public static void add(LauncherHelper.HMCLProcessListener processListener) { + FXUtils.runInFX(() -> { + var holder = new GameProcessHolder(processListener); + processHolders.add(0, holder); + if (display.get()) displayedHolders.add(0, holder); + }); } - public static void addProcessListener(LauncherHelper.HMCLProcessListener processListener) { - FXUtils.runInFX(() -> processHolders.add(new GameProcessHolder(processListener))); + private static void remove(GameProcessHolder holder) { + FXUtils.runInFX(() -> processHolders.remove(holder)); } public static final class GameProcessHolder { @@ -59,6 +92,7 @@ public static final class GameProcessHolder { private final WeakListenerHolder holder = new WeakListenerHolder(); private final String id; + private final HMCLGameInstance instance; private final ObservableList logs = FXCollections.observableArrayList(); private final ReadOnlyStringWrapper lastLogLine = new ReadOnlyStringWrapper(""); @@ -66,6 +100,7 @@ public static final class GameProcessHolder { private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { this.listenerRef = new WeakReference<>(processListener); + this.instance = processListener.getGameInstance(); { String id = processListener.getGameInstance().getId().id(); int i = idToLaunchedCount.computeIfAbsent(id, k -> 0) + 1; @@ -87,9 +122,7 @@ private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { } holder.onWeakChangeAndOperate(processListener.exitedProperty(), b -> { if (b) { - var currentLogs = processListener.getLogWindow().getLogs(); - this.lastLogLine.set(currentLogs.get(currentLogs.size() - 1).getLog()); // I don't know why but this is necessary - processHolders.remove(this); + remove(this); this.exited.set(true); } }); @@ -103,6 +136,10 @@ public String getId() { return id; } + public HMCLGameInstance getInstance() { + return instance; + } + public ObservableList getLogs() { return logs; } @@ -114,6 +151,26 @@ public ReadOnlyStringProperty lastLogLineProperty() { public ReadOnlyBooleanProperty exitedProperty() { return exited.getReadOnlyProperty(); } + + public void relaunch() { + if (exitedProperty().get()) Instances.launch(getInstance()); + } + + public void showLogWindow() { + var listener = getListenerRef().get(); + if (listener != null) { + listener.getLogWindow().show(); + } else { + LogWindow logWindow = new LogWindow(); + logWindow.logLines(getLogs()); + logWindow.show(); + } + } + + public void terminate() { + var listener = getListenerRef().get(); + if (listener != null) listener.getProcess().stop(); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index fc4744dcf5a..60288eaf6bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -950,7 +950,7 @@ public void run() { } } - GameProcessManager.addProcessListener(this); + GameProcessManager.add(this); } private void finishLaunch() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index 7bbc9f41971..df97f77abb2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -19,6 +19,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXListView; +import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -48,7 +49,7 @@ public class GameProcessPage extends ListPageBase implements private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("game.process"))); public GameProcessPage() { - + Bindings.bindContent(getItems(), GameProcessManager.displayedHolders); } @Override @@ -64,19 +65,16 @@ public ReadOnlyObjectProperty stateProperty() { @Override public void refresh() { setLoading(true); - GameProcessManager.cleanupProcessListeners(); - getItems().setAll(GameProcessManager.processHolders); + GameProcessManager.updateDisplay(); setLoading(false); } - @Override public void onPageShown() { - refresh(); + GameProcessManager.setDisplay(true); } - @Override public void onPageHidden() { - getItems().clear(); + GameProcessManager.setDisplay(false); } private static final class GameProcessPageSkin extends ToolbarListPageSkin { @@ -101,6 +99,7 @@ protected ListCell createListCell(JFXListView { private final TwoLineListItem content = new TwoLineListItem(); + private final JFXButton relaunchButton = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); private final JFXButton logWindowButton = FXUtils.newToggleButton4(SVG.TERMINAL); private final JFXButton terminateButton = FXUtils.newToggleButton4(SVG.SHUTDOWN); @@ -113,10 +112,21 @@ public GameProcessCell(JFXListView listView) { HBox.setHgrow(content, Priority.ALWAYS); content.setMouseTransparent(true); + FXUtils.installFastTooltip(relaunchButton, i18n("game.process.relaunch")); FXUtils.installFastTooltip(logWindowButton, i18n("game.process.show_log")); FXUtils.installFastTooltip(terminateButton, i18n("game.process.terminate")); - container.getChildren().setAll(content, logWindowButton, terminateButton); + relaunchButton.setOnAction(event -> { + if (getItem() != null && !isEmpty()) getItem().relaunch(); + }); + logWindowButton.setOnAction(event -> { + if (getItem() != null && !isEmpty()) getItem().showLogWindow(); + }); + terminateButton.setOnAction(event -> { + if (getItem() != null && !isEmpty()) getItem().terminate(); + }); + + container.getChildren().setAll(content, relaunchButton, logWindowButton, terminateButton); StackPane.setMargin(container, new Insets(8)); getContainer().getChildren().setAll(container); } @@ -124,8 +134,7 @@ public GameProcessCell(JFXListView listView) { @Override protected void updateControl(GameProcessHolder item, boolean empty) { if (item == null || empty) { - logWindowButton.setOnAction(null); - terminateButton.setOnAction(null); + relaunchButton.disableProperty().unbind(); terminateButton.disableProperty().unbind(); return; } @@ -133,20 +142,7 @@ protected void updateControl(GameProcessHolder item, boolean empty) { content.setTitle(item.getId()); content.subtitleProperty().bind(item.lastLogLineProperty()); - logWindowButton.setOnAction(event -> { - var listener = item.getListenerRef().get(); - if (listener != null) { - listener.getLogWindow().show(); - } else { - LogWindow logWindow = new LogWindow(); - logWindow.logLines(item.getLogs()); - logWindow.show(); - } - }); - terminateButton.setOnAction(event -> { - var listener = item.getListenerRef().get(); - if (listener != null) listener.getProcess().stop(); - }); + relaunchButton.disableProperty().bind(item.exitedProperty().not()); terminateButton.disableProperty().bind(item.exitedProperty()); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 901588642ed..3886afcc0a9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -733,6 +733,7 @@ game_directory.use_relative_path=Use a relative path for the game directory if p game_directory.root=Using the disk root directory as the game folder may cause the game to fail to launch and will create multiple files and folders in the disk root directory.\n\nAre you sure you want to continue? game.process=Game Process +game.process.relaunch=Re-launch game.process.show_log=Show Log game.process.terminate=Terminate diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties index 324490d211d..9ab679cfe0f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -551,6 +551,7 @@ game_directory.use_relative_path=若可能,游戏文件夹使用相对路径 game_directory.root=使用磁盘根目录作为游戏文件夹可能会导致无法启动游戏,并会在磁盘根目录下创建多个文件(夹)。\n\n确定要继续吗? game.process=游戏进程 +game.process.relaunch=重新启动 game.process.show_log=显示日志 game.process.terminate=终止进程 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties index e058d72117f..ac48eed753a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -545,6 +545,7 @@ game_directory.use_relative_path=如可行,則對於遊戲目錄使用相對 game_directory.root=使用磁碟根目錄作為遊戲資料夾可能會導致無法啟動遊戲,並會在磁碟根目錄下建立多個檔案和資料夾。\n\n確定要繼續嗎? game.process=遊戲程序 +game.process.relaunch=重新啟動 game.process.show_log=顯示日誌 game.process.terminate=終止程序 From a39789cb04dbb9ca9b9997449c5997dc57641ac0 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:19:02 +0800 Subject: [PATCH 05/12] terminate all --- .../org/jackhuang/hmcl/ui/instances/GameProcessPage.java | 9 ++++++++- HMCL/src/main/resources/assets/lang/I18N.properties | 1 + .../main/resources/assets/lang/I18N_zh_Hans.properties | 1 + .../main/resources/assets/lang/I18N_zh_Hant.properties | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index df97f77abb2..42d6a5602d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -77,6 +77,12 @@ public void onPageHidden() { GameProcessManager.setDisplay(false); } + private void terminateSelected(Collection selectedItems) { + for (GameProcessHolder item : selectedItems) { + item.terminate(); + } + } + private static final class GameProcessPageSkin extends ToolbarListPageSkin { public GameProcessPageSkin(GameProcessPage skinnable) { @@ -86,7 +92,8 @@ public GameProcessPageSkin(GameProcessPage skinnable) { @Override protected List initializeToolbar(GameProcessPage skinnable) { return List.of( - ToolbarListPageSkin.createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) + ToolbarListPageSkin.createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + ToolbarListPageSkin.createToolbarButton2(i18n("game.process.terminate_all"), SVG.SHUTDOWN, () -> skinnable.terminateSelected(skinnable.getItems())) ); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 3886afcc0a9..d81f6576755 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -736,6 +736,7 @@ game.process=Game Process game.process.relaunch=Re-launch game.process.show_log=Show Log game.process.terminate=Terminate +game.process.terminate_all=Terminate All help=Help help.doc=HMCL Documentation diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties index 9ab679cfe0f..73d0ad89063 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -554,6 +554,7 @@ game.process=游戏进程 game.process.relaunch=重新启动 game.process.show_log=显示日志 game.process.terminate=终止进程 +game.process.terminate_all=终止所有进程 help=帮助 help.doc=HMCL 帮助文档 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties index ac48eed753a..6fbb66e5b18 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -548,6 +548,7 @@ game.process=遊戲程序 game.process.relaunch=重新啟動 game.process.show_log=顯示日誌 game.process.terminate=終止程序 +game.process.terminate_all=終止所有程序 help=說明 help.doc=HMCL 說明文件 From 1ad2ae68393faf8f3997590ea8614e894026694a Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:29:57 +0800 Subject: [PATCH 06/12] open settings --- .../hmcl/game/GameProcessManager.java | 26 +++++++------------ .../hmcl/ui/instances/GameProcessPage.java | 6 ++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java index c8a0dda0042..80ab9e83d76 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -22,9 +22,11 @@ import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.LogWindow; import org.jackhuang.hmcl.ui.WeakListenerHolder; +import org.jackhuang.hmcl.ui.instances.GameInstancePage; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.FXThread; @@ -128,22 +130,10 @@ private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { }); } - public WeakReference getListenerRef() { - return listenerRef; - } - public String getId() { return id; } - public HMCLGameInstance getInstance() { - return instance; - } - - public ObservableList getLogs() { - return logs; - } - public ReadOnlyStringProperty lastLogLineProperty() { return lastLogLine.getReadOnlyProperty(); } @@ -152,23 +142,27 @@ public ReadOnlyBooleanProperty exitedProperty() { return exited.getReadOnlyProperty(); } + public void openSettings() { + Instances.modifyGameSettings(instance); + } + public void relaunch() { - if (exitedProperty().get()) Instances.launch(getInstance()); + if (exitedProperty().get()) Instances.launch(instance); } public void showLogWindow() { - var listener = getListenerRef().get(); + var listener = listenerRef.get(); if (listener != null) { listener.getLogWindow().show(); } else { LogWindow logWindow = new LogWindow(); - logWindow.logLines(getLogs()); + logWindow.logLines(logs); logWindow.show(); } } public void terminate() { - var listener = getListenerRef().get(); + var listener = listenerRef.get(); if (listener != null) listener.getProcess().stop(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index 42d6a5602d5..ff522437561 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -106,6 +106,7 @@ protected ListCell createListCell(JFXListView { private final TwoLineListItem content = new TwoLineListItem(); + private final JFXButton settingsButton = FXUtils.newToggleButton4(SVG.SETTINGS); private final JFXButton relaunchButton = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); private final JFXButton logWindowButton = FXUtils.newToggleButton4(SVG.TERMINAL); private final JFXButton terminateButton = FXUtils.newToggleButton4(SVG.SHUTDOWN); @@ -123,6 +124,9 @@ public GameProcessCell(JFXListView listView) { FXUtils.installFastTooltip(logWindowButton, i18n("game.process.show_log")); FXUtils.installFastTooltip(terminateButton, i18n("game.process.terminate")); + settingsButton.setOnAction(event -> { + if (getItem() != null && !isEmpty()) getItem().openSettings(); + }); relaunchButton.setOnAction(event -> { if (getItem() != null && !isEmpty()) getItem().relaunch(); }); @@ -133,7 +137,7 @@ public GameProcessCell(JFXListView listView) { if (getItem() != null && !isEmpty()) getItem().terminate(); }); - container.getChildren().setAll(content, relaunchButton, logWindowButton, terminateButton); + container.getChildren().setAll(content, settingsButton, relaunchButton, logWindowButton, terminateButton); StackPane.setMargin(container, new Insets(8)); getContainer().getChildren().setAll(container); } From bfd8f52626879e396e721b83dfc35d81d8bffda2 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:30:40 +0800 Subject: [PATCH 07/12] update --- .../main/java/org/jackhuang/hmcl/game/GameProcessManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java index 80ab9e83d76..3210b5da08d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -22,11 +22,9 @@ import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.LogWindow; import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.instances.GameInstancePage; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.FXThread; From 67725c37f5cf85b21d5a3f91efde7c09d429cba3 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:46:59 +0800 Subject: [PATCH 08/12] alive process count --- .../hmcl/game/GameProcessManager.java | 20 +++++++++---------- .../hmcl/ui/instances/GameProcessPage.java | 11 +++++++++- .../org/jackhuang/hmcl/ui/main/RootPage.java | 3 ++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java index 3210b5da08d..cd8af4d15f0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -19,6 +19,7 @@ import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; +import javafx.beans.binding.IntegerBinding; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -29,11 +30,11 @@ import org.jackhuang.hmcl.util.FXThread; import java.lang.ref.WeakReference; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; +/// +/// @author Calboot public final class GameProcessManager { private GameProcessManager() { @@ -42,13 +43,12 @@ private GameProcessManager() { @FXThread private static final Map idToLaunchedCount = new HashMap<>(); - @FXThread - private static final List processHolders = new ArrayList<>(); + private static final ObservableList aliveProcessHolders = FXCollections.observableArrayList(); - @FXThread public static final ObservableList displayedHolders = FXCollections.observableArrayList(); - @FXThread + public static final IntegerBinding aliveProcessCount = Bindings.size(aliveProcessHolders); + private static final BooleanProperty display = new SimpleBooleanProperty() { @Override public void invalidated() { @@ -67,21 +67,21 @@ public static void setDisplay(boolean display) { public static void updateDisplay() { FXUtils.runInFX(() -> { - processHolders.removeIf(holder -> holder.exited.get()); - displayedHolders.setAll(processHolders); + aliveProcessHolders.removeIf(holder -> holder.exited.get()); + displayedHolders.setAll(aliveProcessHolders); }); } public static void add(LauncherHelper.HMCLProcessListener processListener) { FXUtils.runInFX(() -> { var holder = new GameProcessHolder(processListener); - processHolders.add(0, holder); + aliveProcessHolders.add(0, holder); if (display.get()) displayedHolders.add(0, holder); }); } private static void remove(GameProcessHolder holder) { - FXUtils.runInFX(() -> processHolders.remove(holder)); + FXUtils.runInFX(() -> aliveProcessHolders.remove(holder)); } public static final class GameProcessHolder { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index ff522437561..e3cc38fd590 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -24,6 +24,7 @@ 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.layout.HBox; @@ -31,6 +32,7 @@ import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.game.GameProcessManager; import org.jackhuang.hmcl.game.GameProcessManager.GameProcessHolder; +import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.MDListCell; import org.jackhuang.hmcl.ui.construct.PageAware; @@ -46,10 +48,17 @@ /// @author Calboot public class GameProcessPage extends ListPageBase implements DecoratorPage, PageAware { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("game.process"))); + private final ReadOnlyObjectWrapper state; public GameProcessPage() { Bindings.bindContent(getItems(), GameProcessManager.displayedHolders); + + Label titleLabel = new Label(); + titleLabel.textProperty().bind(GameProcessManager.aliveProcessCount.asString(i18n("game.process") + " (%d)")); + titleLabel.textFillProperty().bind(Themes.titleFillProperty()); + titleLabel.getStyleClass().add("jfx-decorator-title"); + titleLabel.setMinWidth(0); + state = new ReadOnlyObjectWrapper<>(State.fromTitleNode(titleLabel)); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index ce0865f7e88..6228a6f5318 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -21,6 +21,7 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.scene.layout.Region; import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameProcessManager; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.setting.Accounts; @@ -155,7 +156,7 @@ protected Skin(RootPage control) { // fifth item in left sidebar AdvancedListItem gameProcessItem = new AdvancedListItem(); gameProcessItem.setLeftIcon(SVG.TERMINAL); - gameProcessItem.setTitle(i18n("game.process")); + gameProcessItem.titleProperty().bind(GameProcessManager.aliveProcessCount.asString(i18n("game.process") + " (%d)")); gameProcessItem.setOnAction(e -> Controllers.navigate(Controllers.getGameProcessPage())); // sixth item in left sidebar From 3362310fcf5cc58728a3d5f6b699a121a226b706 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:48:09 +0800 Subject: [PATCH 09/12] update --- .../java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index e3cc38fd590..4d15f30c764 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -79,7 +79,9 @@ public void refresh() { } public void onPageShown() { + setLoading(true); GameProcessManager.setDisplay(true); + setLoading(false); } public void onPageHidden() { From c8565fc6e3b780e0a6df8ce05d5b741e567d8e57 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 16:53:38 +0800 Subject: [PATCH 10/12] sync i18n --- HMCL/src/main/resources/assets/lang/I18N_ar.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_de.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_es.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ja.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_lzh.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ru.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_uk.properties | 2 ++ 7 files changed, 14 insertions(+) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 14907b3d8aa..ede00edca83 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -734,8 +734,10 @@ game_directory.use_relative_path=استخدام مسار نسبي لمجلد ا # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=مساعدة help.doc=توثيق HMCL diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 69cc5fbc177..52340ef16b5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -732,8 +732,10 @@ game_directory.use_relative_path=Verwenden Sie, wenn möglich, einen relativen P # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=Hilfe help.doc=HMCL Dokumentation diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 0444f4df230..c721a71cb92 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -726,8 +726,10 @@ game_directory.use_relative_path=Utilizar ruta relativa para la ruta del juego s # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=Ayuda help.doc=Documentación de HMCL diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 04e7ada1670..d9aa808ec83 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -536,8 +536,10 @@ game_directory.use_relative_path=可能であれば、ゲームディレクト # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=ヘルプ help.doc=ドキュメント diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 435d0e1ad90..71e1e928ed5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -548,8 +548,10 @@ game_directory.use_relative_path=苟能是,戲案夾用相對之徑 # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=助 help.doc=HMCL 之案文 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index c7a1b8fcf41..b838d6207a9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -718,8 +718,10 @@ game_directory.use_relative_path=По возможности используй # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=Справка help.doc=Документация лаунчера diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 39e6df7ee7e..7b1f7b4b466 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -687,8 +687,10 @@ game_directory.use_relative_path=Використовувати відносни # game_directory.root= # game.process= +# game.process.relaunch= # game.process.show_log= # game.process.terminate= +# game.process.terminate_all= help=Допомога help.doc=Документація Hello Minecraft! Лаунчера From c042012b45171e8d6e9785dd70a659ee28cc5990 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 13 Sep 2026 21:10:05 +0800 Subject: [PATCH 11/12] update --- .../main/java/org/jackhuang/hmcl/game/GameProcessManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java index cd8af4d15f0..dc987ba15f8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -33,6 +33,7 @@ import java.util.HashMap; import java.util.Map; +/// Manager of game processes launched by HMCL. /// /// @author Calboot public final class GameProcessManager { From 93c1c5b7287d1c18c2f6e232f46acc86fcdcd61c Mon Sep 17 00:00:00 2001 From: ToobLac Date: Mon, 14 Sep 2026 21:10:59 +0800 Subject: [PATCH 12/12] launch game --- .../hmcl/game/GameProcessManager.java | 47 +++++++++---------- .../jackhuang/hmcl/game/LauncherHelper.java | 6 ++- .../java/org/jackhuang/hmcl/ui/LogWindow.java | 7 ++- .../hmcl/ui/instances/GameProcessPage.java | 5 ++ .../org/jackhuang/hmcl/ui/main/MainPage.java | 8 ++++ 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java index dc987ba15f8..77d10ca8f6b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.game; -import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.binding.IntegerBinding; import javafx.beans.property.*; @@ -27,7 +26,9 @@ import org.jackhuang.hmcl.ui.LogWindow; import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.instances.Instances; +import org.jackhuang.hmcl.util.CircularArrayList; import org.jackhuang.hmcl.util.FXThread; +import org.jackhuang.hmcl.util.platform.ManagedProcess; import java.lang.ref.WeakReference; import java.util.HashMap; @@ -87,7 +88,8 @@ private static void remove(GameProcessHolder holder) { public static final class GameProcessHolder { - private final WeakReference listenerRef; + private final WeakReference processRef; + private WeakReference logWindowRef; @SuppressWarnings("FieldCanBeLocal") private final WeakListenerHolder holder = new WeakListenerHolder(); @@ -95,32 +97,22 @@ public static final class GameProcessHolder { private final String id; private final HMCLGameInstance instance; - private final ObservableList logs = FXCollections.observableArrayList(); - private final ReadOnlyStringWrapper lastLogLine = new ReadOnlyStringWrapper(""); + private final CircularArrayList logs; + private final ReadOnlyStringWrapper lastLogLine = new ReadOnlyStringWrapper(null); private final ReadOnlyBooleanWrapper exited = new ReadOnlyBooleanWrapper(); private GameProcessHolder(LauncherHelper.HMCLProcessListener processListener) { - this.listenerRef = new WeakReference<>(processListener); + this.processRef = new WeakReference<>(processListener.getProcess()); + this.logWindowRef = new WeakReference<>(processListener.getLogWindow()); this.instance = processListener.getGameInstance(); + this.logs = processListener.getLogs(); + this.lastLogLine.bind(processListener.getLogWindow().lastLogLineProperty()); { String id = processListener.getGameInstance().getId().id(); int i = idToLaunchedCount.computeIfAbsent(id, k -> 0) + 1; idToLaunchedCount.put(id, i); this.id = id + " #" + i; } - { - Bindings.bindContent(logs, processListener.getLogWindow().getLogs()); - if (!logs.isEmpty()) { - lastLogLine.set(logs.get(logs.size() - 1).getLog()); - } - logs.addListener((InvalidationListener) o -> { - if (!logs.isEmpty()) { - lastLogLine.set(logs.get(logs.size() - 1).getLog()); - } else { - lastLogLine.set(""); - } - }); - } holder.onWeakChangeAndOperate(processListener.exitedProperty(), b -> { if (b) { remove(this); @@ -150,19 +142,22 @@ public void relaunch() { } public void showLogWindow() { - var listener = listenerRef.get(); - if (listener != null) { - listener.getLogWindow().show(); - } else { - LogWindow logWindow = new LogWindow(); + LogWindow logWindow; + LogWindow cached; + if ((cached = logWindowRef.get()) == null) { + logWindow = new LogWindow(); logWindow.logLines(logs); - logWindow.show(); + logWindowRef = new WeakReference<>(logWindow); + } else { + logWindow = cached; } + logWindow.show(); + logWindow.requestFocus(); } public void terminate() { - var listener = listenerRef.get(); - if (listener != null) listener.getProcess().stop(); + var process = processRef.get(); + if (process != null) process.stop(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 60288eaf6bd..c0114e6f385 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -100,7 +100,7 @@ public LauncherHelper(HMCLGameInstance gameInstance, Account account) { this.setting = gameInstance.getEffectiveSettings(); this.launcherVisibility = setting.getInheritable(GameSettings::launcherVisibilityProperty); this.showLogs = setting.getInheritable(GameSettings::showLogsProperty); - this.launchingStepsPane.setTitle(i18n("instance.launch")); + this.launchingStepsPane.setTitle(i18n("instance.launch") + " - " + gameInstance.getId().id()); } public HMCLGameInstance getGameInstance() { @@ -871,6 +871,10 @@ public LogWindow getLogWindow() { return logWindow; } + public CircularArrayList getLogs() { + return logs; + } + public HMCLGameInstance getGameInstance() { return launcherHelper.gameInstance; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index f9a59782d83..c48ad83a3a2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -69,6 +69,7 @@ public final class LogWindow extends Stage { private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG}; private final CircularArrayList logs; + private final ReadOnlyStringWrapper lastLogLine = new ReadOnlyStringWrapper(); private final Map levelCountMap = new EnumMap<>(Log4jLevel.class); private final Map levelShownMap = new EnumMap<>(Log4jLevel.class); @@ -112,6 +113,7 @@ public LogWindow(@Nullable ManagedProcess gameProcess, CircularArrayList lo public void logLine(Log log) { Log4jLevel level = log.getLevel(); logs.add(log); + lastLogLine.set(log.getLog()); if (levelShownMap.get(level).get()) impl.listView.getItems().add(log); @@ -125,6 +127,7 @@ public void logLines(List logs) { for (Log log : logs) { Log4jLevel level = log.getLevel(); this.logs.add(log); + this.lastLogLine.set(log.getLog()); if (levelShownMap.get(level).get()) impl.listView.getItems().add(log); @@ -135,8 +138,8 @@ public void logLines(List logs) { autoScroll(); } - public ObservableList getLogs() { - return impl.listView.getItems(); + public ReadOnlyStringProperty lastLogLineProperty() { + return lastLogLine.getReadOnlyProperty(); } private void shakeLogs() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java index 4d15f30c764..9b610a913b7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -88,6 +88,10 @@ public void onPageHidden() { GameProcessManager.setDisplay(false); } + private void launch() { + Controllers.getRootPage().getMainPage().launchCurrentGame(); + } + private void terminateSelected(Collection selectedItems) { for (GameProcessHolder item : selectedItems) { item.terminate(); @@ -104,6 +108,7 @@ public GameProcessPageSkin(GameProcessPage skinnable) { protected List initializeToolbar(GameProcessPage skinnable) { return List.of( ToolbarListPageSkin.createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + ToolbarListPageSkin.createToolbarButton2(i18n("instance.launch"), SVG.ROCKET_LAUNCH, skinnable::launch), ToolbarListPageSkin.createToolbarButton2(i18n("game.process.terminate_all"), SVG.SHUTDOWN, () -> skinnable.terminateSelected(skinnable.getItems())) ); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 52c32536af8..04b004cd21e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -400,6 +400,14 @@ private void launchNoGame() { Controllers.taskDialog(task, i18n("instance.launch.empty.installing"), TaskCancellationAction.NORMAL); } + public void launchCurrentGame() { + if (getCurrentGame() != null) { + launch(); + } else { + launchNoGame(); + } + } + private void onUpgrade() { RemoteVersion target = UpdateChecker.getLatestVersion(); if (target == null) {