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 0000000000..77d10ca8f6 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/GameProcessManager.java @@ -0,0 +1,164 @@ +/* + * 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.binding.Bindings; +import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.*; +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.CircularArrayList; +import org.jackhuang.hmcl.util.FXThread; +import org.jackhuang.hmcl.util.platform.ManagedProcess; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +/// Manager of game processes launched by HMCL. +/// +/// @author Calboot +public final class GameProcessManager { + + private GameProcessManager() { + } + + @FXThread + private static final Map idToLaunchedCount = new HashMap<>(); + + private static final ObservableList aliveProcessHolders = FXCollections.observableArrayList(); + + public static final ObservableList displayedHolders = FXCollections.observableArrayList(); + + public static final IntegerBinding aliveProcessCount = Bindings.size(aliveProcessHolders); + + 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(() -> { + aliveProcessHolders.removeIf(holder -> holder.exited.get()); + displayedHolders.setAll(aliveProcessHolders); + }); + } + + public static void add(LauncherHelper.HMCLProcessListener processListener) { + FXUtils.runInFX(() -> { + var holder = new GameProcessHolder(processListener); + aliveProcessHolders.add(0, holder); + if (display.get()) displayedHolders.add(0, holder); + }); + } + + private static void remove(GameProcessHolder holder) { + FXUtils.runInFX(() -> aliveProcessHolders.remove(holder)); + } + + public static final class GameProcessHolder { + + private final WeakReference processRef; + private WeakReference logWindowRef; + + @SuppressWarnings("FieldCanBeLocal") + private final WeakListenerHolder holder = new WeakListenerHolder(); + + private final String id; + private final HMCLGameInstance instance; + + private final CircularArrayList logs; + private final ReadOnlyStringWrapper lastLogLine = new ReadOnlyStringWrapper(null); + private final ReadOnlyBooleanWrapper exited = new ReadOnlyBooleanWrapper(); + + private GameProcessHolder(LauncherHelper.HMCLProcessListener 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; + } + holder.onWeakChangeAndOperate(processListener.exitedProperty(), b -> { + if (b) { + remove(this); + this.exited.set(true); + } + }); + } + + public String getId() { + return id; + } + + public ReadOnlyStringProperty lastLogLineProperty() { + return lastLogLine.getReadOnlyProperty(); + } + + public ReadOnlyBooleanProperty exitedProperty() { + return exited.getReadOnlyProperty(); + } + + public void openSettings() { + Instances.modifyGameSettings(instance); + } + + public void relaunch() { + if (exitedProperty().get()) Instances.launch(instance); + } + + public void showLogWindow() { + LogWindow logWindow; + LogWindow cached; + if ((cached = logWindowRef.get()) == null) { + logWindow = new LogWindow(); + logWindow.logLines(logs); + logWindowRef = new WeakReference<>(logWindow); + } else { + logWindow = cached; + } + logWindow.show(); + logWindow.requestFocus(); + } + + public void terminate() { + 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 aaef6607bc..c0114e6f38 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,8 @@ package org.jackhuang.hmcl.game; import com.jfoenix.controls.JFXButton; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.*; @@ -47,6 +49,7 @@ 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; @@ -97,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() { @@ -290,7 +293,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 +837,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 +852,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 +863,26 @@ 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 CircularArrayList getLogs() { + return logs; + } + + public HMCLGameInstance getGameInstance() { + return launcherHelper.gameInstance; + } + + public ReadOnlyBooleanProperty exitedProperty() { + return exited.getReadOnlyProperty(); + } + @Override public void setProcess(ManagedProcess process) { this.process = process; @@ -869,11 +896,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 +953,12 @@ public void run() { Thread.currentThread().interrupt(); } } + + GameProcessManager.add(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 +1005,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 +1027,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 +1054,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 52411fb21b..2504d99326 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 6ac9d2daf2..c48ad83a3a 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,7 @@ 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 java.io.IOException; import java.nio.file.Files; @@ -68,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); @@ -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,12 +107,13 @@ 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) { Log4jLevel level = log.getLevel(); logs.add(log); + lastLogLine.set(log.getLog()); if (levelShownMap.get(level).get()) impl.listView.getItems().add(log); @@ -120,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); @@ -130,6 +138,10 @@ public void logLines(List logs) { autoScroll(); } + public ReadOnlyStringProperty lastLogLineProperty() { + return lastLogLine.getReadOnlyProperty(); + } + private void shakeLogs() { impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList())); autoScroll(); @@ -198,7 +210,7 @@ private final class LogWindowImpl extends Control { } private void onTerminateGame() { - LogWindow.this.gameProcess.stop(); + if (gameProcess != null) gameProcess.stop(); } private void onClear() { @@ -234,7 +246,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 +263,7 @@ private void onExportDump(SpinnerPane pane) { }); } - private ManagedProcess getGameProcess() { + private @Nullable ManagedProcess getGameProcess() { return gameProcess; } @@ -433,12 +445,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 9991ac13b8..79948c8bb3 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 0000000000..9b610a913b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameProcessPage.java @@ -0,0 +1,177 @@ +/* + * 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.binding.Bindings; +import javafx.beans.property.*; +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; +import javafx.scene.layout.Priority; +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; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + +import java.util.*; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +/// Page for managing running game processes. +/// +/// @author Calboot +public class GameProcessPage extends ListPageBase implements DecoratorPage, PageAware { + + 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 + protected Skin createDefaultSkin() { + return new GameProcessPageSkin(this); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } + + @Override + public void refresh() { + setLoading(true); + GameProcessManager.updateDisplay(); + setLoading(false); + } + + public void onPageShown() { + setLoading(true); + GameProcessManager.setDisplay(true); + setLoading(false); + } + + public void onPageHidden() { + GameProcessManager.setDisplay(false); + } + + private void launch() { + Controllers.getRootPage().getMainPage().launchCurrentGame(); + } + + private void terminateSelected(Collection selectedItems) { + for (GameProcessHolder item : selectedItems) { + item.terminate(); + } + } + + 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), + ToolbarListPageSkin.createToolbarButton2(i18n("instance.launch"), SVG.ROCKET_LAUNCH, skinnable::launch), + ToolbarListPageSkin.createToolbarButton2(i18n("game.process.terminate_all"), SVG.SHUTDOWN, () -> skinnable.terminateSelected(skinnable.getItems())) + ); + } + + @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 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); + + 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(relaunchButton, i18n("game.process.relaunch")); + 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(); + }); + logWindowButton.setOnAction(event -> { + if (getItem() != null && !isEmpty()) getItem().showLogWindow(); + }); + terminateButton.setOnAction(event -> { + if (getItem() != null && !isEmpty()) getItem().terminate(); + }); + + container.getChildren().setAll(content, settingsButton, relaunchButton, logWindowButton, terminateButton); + StackPane.setMargin(container, new Insets(8)); + getContainer().getChildren().setAll(container); + } + + @Override + protected void updateControl(GameProcessHolder item, boolean empty) { + if (item == null || empty) { + relaunchButton.disableProperty().unbind(); + terminateButton.disableProperty().unbind(); + return; + } + + content.setTitle(item.getId()); + content.subtitleProperty().bind(item.lastLogLineProperty()); + + relaunchButton.disableProperty().bind(item.exitedProperty().not()); + terminateButton.disableProperty().bind(item.exitedProperty()); + } + } + +} 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 52c32536af..04b004cd21 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) { 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 46662c4365..6228a6f531 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; @@ -153,6 +154,12 @@ protected Skin(RootPage control) { } // fifth item in left sidebar + AdvancedListItem gameProcessItem = new AdvancedListItem(); + gameProcessItem.setLeftIcon(SVG.TERMINAL); + gameProcessItem.titleProperty().bind(GameProcessManager.aliveProcessCount.asString(i18n("game.process") + " (%d)")); + 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 4e4a249359..d81f657675 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -732,6 +732,12 @@ 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.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 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 5111f7490b..ede00edca8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -733,6 +733,12 @@ game_directory.new=مجلد جديد 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 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 6d474be5f7..52340ef16b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -731,6 +731,12 @@ 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.relaunch= +# game.process.show_log= +# game.process.terminate= +# game.process.terminate_all= + 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 d61b585a42..c721a71cb9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -725,6 +725,12 @@ 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.relaunch= +# game.process.show_log= +# game.process.terminate= +# game.process.terminate_all= + 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 323189a2bd..d9aa808ec8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -535,6 +535,12 @@ game_directory.new=新規作成 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=ドキュメント 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 5d23c25050..71e1e928ed 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -547,6 +547,12 @@ game_directory.new=另增戲案夾 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 之案文 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 df6eca6224..1f026cc4f5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -662,6 +662,12 @@ game_directory.new=Новая папка game_directory.use_relative_path=По возможности использовать относительный путь к папке игры game_directory.root=Если использовать корень диска как папку игры, игра может не запуститься, а в корне диска появится множество файлов и папок.\n\nВы уверены, что хотите продолжить? +# game.process= +# game.process.relaunch= +# game.process.show_log= +# game.process.terminate= +# game.process.terminate_all= + 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 4afbf45756..7b1f7b4b46 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -686,6 +686,12 @@ game_directory.new=Новий каталог 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! Лаунчера 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 797ed0852b..73d0ad8906 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,12 @@ game_directory.new=添加游戏文件夹 game_directory.use_relative_path=若可能,游戏文件夹使用相对路径 game_directory.root=使用磁盘根目录作为游戏文件夹可能会导致无法启动游戏,并会在磁盘根目录下创建多个文件(夹)。\n\n确定要继续吗? +game.process=游戏进程 +game.process.relaunch=重新启动 +game.process.show_log=显示日志 +game.process.terminate=终止进程 +game.process.terminate_all=终止所有进程 + 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 3c3f352714..6fbb66e5b1 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,12 @@ game_directory.new=建立新目錄 game_directory.use_relative_path=如可行,則對於遊戲目錄使用相對路徑 game_directory.root=使用磁碟根目錄作為遊戲資料夾可能會導致無法啟動遊戲,並會在磁碟根目錄下建立多個檔案和資料夾。\n\n確定要繼續嗎? +game.process=遊戲程序 +game.process.relaunch=重新啟動 +game.process.show_log=顯示日誌 +game.process.terminate=終止程序 +game.process.terminate_all=終止所有程序 + help=說明 help.doc=HMCL 說明文件 help.detail=可查閱資料包、模組包製作教學等內容