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 aaef6607bc..fa9a714081 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -88,6 +88,10 @@ public final class LauncherHelper { private final GameSettings.Effective setting; private LauncherVisibility launcherVisibility; private boolean showLogs; + + /// The crash window that hosted the launch progress of a restart; closed once the restart + /// launch succeeds, kept open on failure for inspecting the logs. + private @Nullable GameCrashWindow restartHostWindow; private QuickPlayOption quickPlayOption; private boolean disableOfflineSkin = false; @@ -149,6 +153,23 @@ public void makeLaunchScript(Path scriptFile) { launch(); } + /// Restarts the crashed game with the launch context captured by this helper, e.g. the account + /// and the QuickPlay option. The launch progress is shown as an overlay in the hosting crash + /// window, which is closed when the launch succeeds and stays open on failure. + /// + /// @param hostWindow the crash window that invoked this restart + private void restartCrashedGame(GameCrashWindow hostWindow) { + if (launcherVisibility == LauncherVisibility.HIDE) { + // The platform must outlive the crash window, which closes when the launch succeeds. + setImplicitExit(false); + } + + restartHostWindow = Objects.requireNonNull(hostWindow); + + LOG.info("Restarting game instance: " + gameInstance.getId()); + launch0(); + } + /// Builds and executes the launch pipeline for the captured game instance. private void launch0() { // https://github.com/HMCL-dev/HMCL/pull/4121 @@ -333,6 +354,13 @@ private void launch0() { @Override public void onStop(boolean success, TaskExecutor executor) { runLater(() -> { + if (success && restartHostWindow != null) { + restartHostWindow.close(); + restartHostWindow = null; + } else if (!success && launcherVisibility == LauncherVisibility.HIDE) { + setImplicitExit(true); + } + // Check if the application has stopped // because onStop will be invoked if tasks fail when the executor service shut down. if (!Controllers.isStopped()) { @@ -951,6 +979,8 @@ private void finishLaunch() { // If application was stopped and execution services did not finish termination, // these codes will be executed. if (Controllers.getStage() != null) { + // The platform must survive the main window being closed while the game runs. + setImplicitExit(false); Controllers.getStage().close(); Controllers.shutdown(); Schedulers.shutdown(); @@ -1034,7 +1064,7 @@ public void onExit(int exitCode, ExitType exitType) { if (exitType != ExitType.NORMAL) { gameInstance.markLaunchedAbnormally(); - runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show()); + runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs, launchingStepsPane, LauncherHelper.this::restartCrashedGame).show()); } checkExit(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index 380357070c..c4d1a906b9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -28,6 +28,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; @@ -40,6 +41,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.theme.Themes; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; @@ -61,6 +63,7 @@ import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -87,14 +90,24 @@ public class GameCrashWindow extends Stage { private final List logs; - public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, HMCLGameInstance gameInstance, LaunchOptions launchOptions, List logs) { - Themes.applyNativeDarkMode(this); + /// The action invoked when the user clicks the restart button, relaunching the crashed game + /// with the original launch context. It receives this crash window, which hosts the launch + /// progress overlay and is closed by the launcher when the launch succeeds. + private final Consumer restartAction; + + /// The launch progress pane shown as an overlay in this window while the restarted game is + /// launching. + private final Region launchingPane; + public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, HMCLGameInstance gameInstance, LaunchOptions launchOptions, List logs, Region launchingPane, Consumer restartAction) { + Themes.applyNativeDarkMode(this); this.managedProcess = managedProcess; this.exitType = exitType; this.gameInstance = gameInstance; this.launchOptions = launchOptions; this.logs = logs; + this.launchingPane = Objects.requireNonNull(launchingPane); + this.restartAction = Objects.requireNonNull(restartAction); memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " " + i18n("settings.memory.unit.mib")).orElse("-"); @@ -467,7 +480,16 @@ private final class View extends VBox { toolBar.setPadding(new Insets(8)); toolBar.setSpacing(8); toolBar.getStyleClass().add("jfx-tool-bar"); - toolBar.getChildren().setAll(exportButtonPane, logButton, helpButton); + + JFXButton restartButton = FXUtils.newRaisedButton(i18n("game.crash.restart")); + launchingPane.addEventHandler(DialogCloseEvent.CLOSE, event -> restartButton.setDisable(false)); + restartButton.setOnAction(e -> { + restartButton.setDisable(true); + DialogUtils.show(stackPane, launchingPane); + restartAction.accept(GameCrashWindow.this); + }); + + toolBar.getChildren().setAll(exportButtonPane, restartButton, logButton, helpButton); } getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 9a56febc90..bf4f358a99 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -717,6 +717,7 @@ game.crash.reason.unsatisfied_link_error=Failed to launch Minecraft because of m If you have not, please check if you have missing dependency mods.\n\ \n\ Otherwise, if you believe this is caused by HMCL, please provide feedback to us. +game.crash.restart=Restart Game game.crash.title=Game Crashed game.directory=Game Path game.version=Game Instance 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 be22b30fc3..8e58f3dae1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -535,6 +535,7 @@ game.crash.reason.stacktrace=原因未知。请点击日志按钮查看详细信 game.crash.reason.too_old_java=当前游戏由于 Java 版本过低,无法继续运行。\n你需要在“(全局/实例特定) 游戏设置 → 游戏 Java”中更换 Java %1$s 或更新版本的 Java,并重新启动游戏。如果没有下载安装,你可以点击 此处 下载 Microsoft JDK。 game.crash.reason.unknown=原因未知。请点击日志按钮查看详细信息。 game.crash.reason.unsatisfied_link_error=当前游戏由于缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在“(全局/实例特定) 游戏设置 → 高级设置”中修改了本地库路径选项,请你修改回默认模式。\n如果你正在使用默认模式,请检查游戏文件夹路径是否只包含英文字母、数字和下划线。\n如果是,那么请检查是否为模组或 HMCL 导致了本地库缺失的问题。如果你确定是 HMCL 引起的,建议你向我们反馈。\n对于 Windows 用户,你还可以尝试在“控制面板 → 时钟和区域 → 区域 → 管理 → 更改系统区域设置”中将“当前系统区域设置”修改为“中文(简体,中国大陆)”,并关闭“Beta 版:使用 Unicode UTF-8 提供全球语言支持”选项;\n或将游戏文件夹路径中的所有非英文字符的名称 (例如中文、空格等) 修改为英文字符。\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库! +game.crash.restart=重启游戏 game.crash.title=游戏意外退出 game.directory=游戏文件夹路径 game.version=游戏实例 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 458f32c556..e4d25287eb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -529,6 +529,7 @@ game.crash.reason.stacktrace=原因未知。請點擊日誌按鈕查看詳細訊 game.crash.reason.too_old_java=目前遊戲由於 Java 版本過低,無法繼續執行。\n你需要在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中更換 %1$s 或更高版本的 Java,並重新啟動遊戲。如果沒有下載安裝,你可以點擊 此處 下載 Microsoft JDK。 game.crash.reason.unknown=原因未知。請點擊日誌按鈕查看詳細訊息。 game.crash.reason.unsatisfied_link_error=目前遊戲由於缺少本機庫,無法繼續執行。\n這些本機庫缺失:%1$s。\n如果你在「(全域/實例特定) 遊戲設定 → 進階設定」中修改了本機庫路徑選項,請你修改回預設模式。\n如果你正在使用預設模式,請檢查遊戲目錄路徑是否只包含英文字母、數字和底線。\n如果是,那麼請檢查是否為模組或 HMCL 引起的本機庫缺失的問題。如果你確定是 HMCL 引起的,建議你向我們回報。\n對於 Windows 使用者,你還可以嘗試在「控制台 → 時鐘和區域 → 地區 → 系統管理 → 變更系統區域設定」中,關閉「Beta:使用 Unicode UTF-8 提供全球語言支援」選項;\n或將遊戲目錄路徑中的所有非英文字母的名稱 (例如中文、空格等) 修改為英文字母。\n如果你確實需要自訂本機庫路徑,你需要保證其中包含缺失的本機庫! +game.crash.restart=重新啟動遊戲 game.crash.title=遊戲意外退出 game.directory=遊戲目錄路徑 game.version=遊戲實例