mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-08 11:25:46 -04:00
parent
6409841cca
commit
8a816f7f35
@ -55,18 +55,13 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class LauncherHelper {
|
||||
@ -717,14 +712,14 @@ public final class LauncherHelper {
|
||||
private final Version version;
|
||||
private final LaunchOptions launchOptions;
|
||||
private ManagedProcess process;
|
||||
private boolean lwjgl;
|
||||
private volatile boolean lwjgl;
|
||||
private LogWindow logWindow;
|
||||
private final boolean detectWindow;
|
||||
private final ArrayDeque<String> logs;
|
||||
private final ArrayDeque</*Log4jLevel*/Object> levels;
|
||||
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||
private final CircularArrayList<Log> logs;
|
||||
private final CountDownLatch launchingLatch;
|
||||
private final String forbiddenAccessToken;
|
||||
private Thread submitLogThread;
|
||||
private LinkedBlockingQueue<Log> logBuffer;
|
||||
|
||||
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
@ -733,10 +728,7 @@ public final class LauncherHelper {
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
|
||||
|
||||
final int numLogs = config().getLogLines() + 1;
|
||||
this.logs = new ArrayDeque<>(numLogs);
|
||||
this.levels = new ArrayDeque<>(numLogs);
|
||||
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -752,12 +744,60 @@ public final class LauncherHelper {
|
||||
LOG.info("Process ClassPath: " + classpath);
|
||||
}
|
||||
|
||||
if (showLogs)
|
||||
if (showLogs) {
|
||||
CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow(process);
|
||||
logWindow.showNormal();
|
||||
logWindow = new LogWindow(process, logs);
|
||||
logWindow.show();
|
||||
logWindowLatch.countDown();
|
||||
});
|
||||
|
||||
logBuffer = new LinkedBlockingQueue<>();
|
||||
submitLogThread = Lang.thread(new Runnable() {
|
||||
private final ArrayList<Log> currentLogs = new ArrayList<>();
|
||||
private final Semaphore semaphore = new Semaphore(0);
|
||||
|
||||
private void submitLogs() {
|
||||
if (currentLogs.size() == 1) {
|
||||
Log log = currentLogs.get(0);
|
||||
Platform.runLater(() -> logWindow.logLine(log));
|
||||
} else {
|
||||
Platform.runLater(() -> {
|
||||
logWindow.logLines(currentLogs);
|
||||
semaphore.release();
|
||||
});
|
||||
semaphore.acquireUninterruptibly();
|
||||
}
|
||||
currentLogs.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
currentLogs.add(logBuffer.take());
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(200); // Wait for more logs
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
logBuffer.drainTo(currentLogs);
|
||||
submitLogs();
|
||||
}
|
||||
|
||||
do {
|
||||
submitLogs();
|
||||
} while (logBuffer.drainTo(currentLogs) > 0);
|
||||
}
|
||||
}, "Game Log Submitter", true);
|
||||
|
||||
try {
|
||||
logWindowLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void finishLaunch() {
|
||||
@ -796,44 +836,37 @@ public final class LauncherHelper {
|
||||
|
||||
@Override
|
||||
public void onLog(String log, boolean isErrorStream) {
|
||||
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
|
||||
|
||||
if (isErrorStream)
|
||||
System.err.println(filteredLog);
|
||||
System.err.println(log);
|
||||
else
|
||||
System.out.println(filteredLog);
|
||||
System.out.println(log);
|
||||
|
||||
Log4jLevel level;
|
||||
if (isErrorStream && !filteredLog.startsWith("[authlib-injector]"))
|
||||
level = Log4jLevel.ERROR;
|
||||
else
|
||||
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
|
||||
|
||||
synchronized (this) {
|
||||
logs.add(filteredLog);
|
||||
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
|
||||
if (logs.size() > config().getLogLines()) {
|
||||
logs.removeFirst();
|
||||
levels.removeFirst();
|
||||
}
|
||||
}
|
||||
log = StringUtils.parseEscapeSequence(log);
|
||||
if (forbiddenAccessToken != null)
|
||||
log = log.replace(forbiddenAccessToken, "<access token>");
|
||||
|
||||
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
|
||||
if (showLogs) {
|
||||
try {
|
||||
logWindowLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
if (level == null)
|
||||
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||
logBuffer.add(new Log(log, level));
|
||||
} else {
|
||||
synchronized (this) {
|
||||
logs.addLast(new Log(log, level));
|
||||
if (logs.size() > Log.getLogLines())
|
||||
logs.removeFirst();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
|
||||
}
|
||||
|
||||
if (!lwjgl) {
|
||||
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
|
||||
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
|
||||
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
synchronized (this) {
|
||||
if (!lwjgl) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -841,7 +874,13 @@ public final class LauncherHelper {
|
||||
@Override
|
||||
public void onExit(int exitCode, ExitType exitType) {
|
||||
if (showLogs) {
|
||||
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
submitLogThread.interrupt();
|
||||
try {
|
||||
submitLogThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
launchingLatch.countDown();
|
||||
@ -850,14 +889,16 @@ public final class LauncherHelper {
|
||||
return;
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) finishLaunch();
|
||||
if (!lwjgl) {
|
||||
synchronized (this) {
|
||||
if (!lwjgl)
|
||||
finishLaunch();
|
||||
}
|
||||
}
|
||||
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
|
||||
Lang.forEachZipped(logs, levels,
|
||||
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
|
||||
repository.markVersionLaunchedAbnormally(version.getId());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
|
||||
}
|
||||
|
||||
checkExit();
|
||||
|
72
HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java
Normal file
72
HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
public final class Log {
|
||||
public static final int DEFAULT_LOG_LINES = 2000;
|
||||
|
||||
public static int getLogLines() {
|
||||
Integer lines = config().getLogLines();
|
||||
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
|
||||
}
|
||||
|
||||
private final String log;
|
||||
private Log4jLevel level;
|
||||
private boolean selected = false;
|
||||
|
||||
public Log(String log) {
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public Log(String log, Log4jLevel level) {
|
||||
this.log = log;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getLog() {
|
||||
return log;
|
||||
}
|
||||
|
||||
public Log4jLevel getLevel() {
|
||||
Log4jLevel level = this.level;
|
||||
if (level == null) {
|
||||
level = Log4jLevel.guessLevel(log);
|
||||
if (level == null)
|
||||
level = Log4jLevel.INFO;
|
||||
this.level = level;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return log;
|
||||
}
|
||||
}
|
@ -164,7 +164,7 @@ public final class Config implements Cloneable, Observable {
|
||||
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("logLines")
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
|
||||
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
|
||||
|
||||
@SerializedName("titleTransparent")
|
||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
@ -573,15 +573,15 @@ public final class Config implements Cloneable, Observable {
|
||||
this.launcherFontFamily.set(launcherFontFamily);
|
||||
}
|
||||
|
||||
public int getLogLines() {
|
||||
public Integer getLogLines() {
|
||||
return logLines.get();
|
||||
}
|
||||
|
||||
public void setLogLines(int logLines) {
|
||||
public void setLogLines(Integer logLines) {
|
||||
this.logLines.set(logLines);
|
||||
}
|
||||
|
||||
public IntegerProperty logLinesProperty() {
|
||||
public ObjectProperty<Integer> logLinesProperty() {
|
||||
return logLines;
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
@ -998,4 +999,11 @@ public final class FXUtils {
|
||||
return tf;
|
||||
}
|
||||
|
||||
public static String toWeb(Color color) {
|
||||
int r = (int) Math.round(color.getRed() * 255.0);
|
||||
int g = (int) Math.round(color.getGreen() * 255.0);
|
||||
int b = (int) Math.round(color.getBlue() * 255.0);
|
||||
|
||||
return String.format("#%02x%02x%02x", r, g, b);
|
||||
}
|
||||
}
|
||||
|
@ -89,9 +89,9 @@ public class GameCrashWindow extends Stage {
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final Collection<Pair<String, Log4jLevel>> logs;
|
||||
private final List<Log> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
@ -124,7 +124,7 @@ public class GameCrashWindow extends Stage {
|
||||
private void analyzeCrashReport() {
|
||||
loading.set(true);
|
||||
Task.allOf(Task.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
|
||||
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
|
||||
|
||||
// Get the crash-report from the crash-reports/xxx, or the output of console.
|
||||
String crashReport = null;
|
||||
@ -264,20 +264,18 @@ public class GameCrashWindow extends Stage {
|
||||
private void showLogWindow() {
|
||||
LogWindow logWindow = new LogWindow(managedProcess);
|
||||
|
||||
logWindow.logLine(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
|
||||
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
|
||||
if (managedProcess.getClasspath() != null)
|
||||
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
|
||||
logWindow.showNormal();
|
||||
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
|
||||
logWindow.logLines(logs);
|
||||
logWindow.show();
|
||||
}
|
||||
|
||||
private void exportGameCrashInfo() {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.handleAsync((result, exception) -> {
|
||||
|
@ -25,8 +25,8 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@ -38,13 +38,14 @@ import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -54,14 +55,11 @@ import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
@ -69,128 +67,120 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public final class LogWindow extends Stage {
|
||||
|
||||
private final ArrayDeque<Log> logs = new ArrayDeque<>();
|
||||
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
|
||||
{
|
||||
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
|
||||
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||
|
||||
private final CircularArrayList<Log> logs;
|
||||
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
|
||||
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
|
||||
|
||||
{
|
||||
for (Log4jLevel level : Log4jLevel.values()) {
|
||||
levelCountMap.put(level, new SimpleIntegerProperty());
|
||||
levelShownMap.put(level, new SimpleBooleanProperty(true));
|
||||
}
|
||||
};
|
||||
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
|
||||
{
|
||||
for (Log4jLevel level : Log4jLevel.values()) {
|
||||
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
|
||||
put(level, property);
|
||||
}
|
||||
}
|
||||
};
|
||||
private final LogWindowImpl impl = new LogWindowImpl();
|
||||
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||
|
||||
private Consumer<String> exportGameCrashInfoCallback;
|
||||
|
||||
private boolean stopCheckLogCount = false;
|
||||
}
|
||||
|
||||
private final LogWindowImpl impl;
|
||||
private final ManagedProcess gameProcess;
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess) {
|
||||
this(gameProcess, new CircularArrayList<>());
|
||||
}
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
|
||||
this.logs = logs;
|
||||
this.impl = new LogWindowImpl();
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("logwindow.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||
for (SimpleBooleanProperty property : levelShownMap.values()) {
|
||||
property.addListener(o -> shakeLogs());
|
||||
}
|
||||
|
||||
this.gameProcess = gameProcess;
|
||||
}
|
||||
|
||||
public void logLine(String filteredLine, Log4jLevel level) {
|
||||
Log log = new Log(parseEscapeSequence(filteredLine), level);
|
||||
public void logLine(Log log) {
|
||||
Log4jLevel level = log.getLevel();
|
||||
logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
|
||||
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
|
||||
if (!stopCheckLogCount) checkLogCount();
|
||||
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
|
||||
property.set(property.get() + 1);
|
||||
checkLogCount();
|
||||
autoScroll();
|
||||
}
|
||||
|
||||
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
|
||||
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
|
||||
this.impl.showCrashReport.set(true);
|
||||
stopCheckLogCount = true;
|
||||
for (Log log : impl.listView.getItems()) {
|
||||
if (log.log.contains("Minecraft Crash Report")) {
|
||||
Platform.runLater(() -> {
|
||||
impl.listView.scrollTo(log);
|
||||
});
|
||||
break;
|
||||
}
|
||||
public void logLines(List<Log> logs) {
|
||||
for (Log log : logs) {
|
||||
Log4jLevel level = log.getLevel();
|
||||
this.logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
|
||||
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
|
||||
property.set(property.get() + 1);
|
||||
}
|
||||
show();
|
||||
}
|
||||
|
||||
public void showNormal() {
|
||||
this.impl.showCrashReport.set(false);
|
||||
show();
|
||||
checkLogCount();
|
||||
autoScroll();
|
||||
}
|
||||
|
||||
private void shakeLogs() {
|
||||
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
|
||||
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList()));
|
||||
autoScroll();
|
||||
}
|
||||
|
||||
private void checkLogCount() {
|
||||
while (logs.size() > config().getLogLines()) {
|
||||
int nRemove = logs.size() - Log.getLogLines();
|
||||
if (nRemove <= 0)
|
||||
return;
|
||||
|
||||
ObservableList<Log> items = impl.listView.getItems();
|
||||
int itemsSize = items.size();
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < nRemove; i++) {
|
||||
Log removedLog = logs.removeFirst();
|
||||
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
|
||||
impl.listView.getItems().remove(0);
|
||||
}
|
||||
if (itemsSize > count && items.get(count) == removedLog)
|
||||
count++;
|
||||
}
|
||||
|
||||
items.remove(0, count);
|
||||
}
|
||||
|
||||
private static class Log {
|
||||
private final String log;
|
||||
private final Log4jLevel level;
|
||||
private boolean selected = false;
|
||||
|
||||
public Log(String log, Log4jLevel level) {
|
||||
this.log = log;
|
||||
this.level = level;
|
||||
}
|
||||
private void autoScroll() {
|
||||
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
|
||||
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
|
||||
}
|
||||
|
||||
public class LogWindowImpl extends Control {
|
||||
private final class LogWindowImpl extends Control {
|
||||
|
||||
private final ListView<Log> listView = new JFXListView<>();
|
||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
|
||||
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
|
||||
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
boolean flag = false;
|
||||
cboLines.getItems().setAll("500", "2000", "5000", "10000");
|
||||
for (String i : cboLines.getItems())
|
||||
if (Integer.toString(config().getLogLines()).equals(i)) {
|
||||
cboLines.getSelectionModel().select(i);
|
||||
flag = true;
|
||||
}
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
buttonText[i] = new SimpleStringProperty();
|
||||
showLevel[i] = new SimpleBooleanProperty(true);
|
||||
}
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(2);
|
||||
cboLines.getItems().setAll(500, 2000, 5000, 10000);
|
||||
cboLines.setValue(Log.getLogLines());
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue));
|
||||
});
|
||||
|
||||
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
|
||||
levelShownMap.get(levels[i]).bind(showLevel.get(i));
|
||||
for (int i = 0; i < LEVELS.length; ++i) {
|
||||
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
|
||||
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,7 +197,7 @@ public final class LogWindow extends Stage {
|
||||
thread(() -> {
|
||||
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
try {
|
||||
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
|
||||
Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList()));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to export logs", e);
|
||||
return;
|
||||
@ -223,38 +213,31 @@ public final class LogWindow extends Stage {
|
||||
});
|
||||
}
|
||||
|
||||
private void onExportDump(JFXButton button) {
|
||||
private void onExportDump(SpinnerPane pane) {
|
||||
assert SystemUtils.supportJVMAttachment();
|
||||
|
||||
pane.setLoading(true);
|
||||
|
||||
thread(() -> {
|
||||
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
|
||||
if (SystemUtils.supportJVMAttachment()) {
|
||||
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
|
||||
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
|
||||
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()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to create minecraft jstack dump", e);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button"));
|
||||
alert.setTitle(i18n("message.error"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
|
||||
try {
|
||||
if (gameProcess.isRunning()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to create minecraft jstack dump", e);
|
||||
|
||||
private void onExportGameCrashInfo() {
|
||||
if (exportGameCrashInfoCallback == null) return;
|
||||
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
|
||||
alert.setTitle(i18n("message.error"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> pane.setLoading(false));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -263,7 +246,7 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
@ -275,17 +258,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
private final Set<ListCell<Log>> selected = new HashSet<>();
|
||||
|
||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
button.setStyle("-fx-background-color: " + backgroundColor + ";");
|
||||
button.getStyleClass().add("log-toggle");
|
||||
button.textProperty().bind(buttonText);
|
||||
button.setSelected(true);
|
||||
showLevel.bind(button.selectedProperty());
|
||||
return button;
|
||||
}
|
||||
|
||||
protected LogWindowSkin(LogWindowImpl control) {
|
||||
LogWindowSkin(LogWindowImpl control) {
|
||||
super(control);
|
||||
|
||||
VBox vbox = new VBox(3);
|
||||
@ -310,13 +283,16 @@ public final class LogWindow extends Stage {
|
||||
|
||||
{
|
||||
HBox hBox = new HBox(3);
|
||||
hBox.getChildren().setAll(
|
||||
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
|
||||
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
|
||||
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
|
||||
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
|
||||
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
|
||||
);
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
|
||||
button.getStyleClass().add("log-toggle");
|
||||
button.textProperty().bind(control.buttonText[i]);
|
||||
button.setSelected(true);
|
||||
control.showLevel[i].bind(button.selectedProperty());
|
||||
hBox.getChildren().add(button);
|
||||
}
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
|
||||
@ -347,11 +323,11 @@ public final class LogWindow extends Stage {
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if (!event.isControlDown()) {
|
||||
for (ListCell<Log> logListCell: selected) {
|
||||
for (ListCell<Log> logListCell : selected) {
|
||||
if (logListCell != this) {
|
||||
logListCell.pseudoClassStateChanged(SELECTED, false);
|
||||
if (logListCell.getItem() != null) {
|
||||
logListCell.getItem().selected = false;
|
||||
logListCell.getItem().setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -362,7 +338,7 @@ public final class LogWindow extends Stage {
|
||||
selected.add(this);
|
||||
pseudoClassStateChanged(SELECTED, true);
|
||||
if (getItem() != null) {
|
||||
getItem().selected = true;
|
||||
getItem().setSelected(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -377,18 +353,18 @@ public final class LogWindow extends Stage {
|
||||
lastCell.value = this;
|
||||
|
||||
pseudoClassStateChanged(EMPTY, empty);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
||||
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
|
||||
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(SELECTED, !empty && item.selected);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
|
||||
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
|
||||
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.log);
|
||||
setText(item.getLog());
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -398,10 +374,9 @@ public final class LogWindow extends Stage {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (Log item : listView.getItems()) {
|
||||
if (item != null && item.selected) {
|
||||
if (item.log != null) {
|
||||
stringBuilder.append(item.log);
|
||||
}
|
||||
if (item != null && item.isSelected()) {
|
||||
if (item.getLog() != null)
|
||||
stringBuilder.append(item.getLog());
|
||||
stringBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
@ -417,11 +392,6 @@ public final class LogWindow extends Stage {
|
||||
{
|
||||
BorderPane bottom = new BorderPane();
|
||||
|
||||
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
|
||||
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
|
||||
bottom.setLeft(exportGameCrashInfoButton);
|
||||
|
||||
HBox hBox = new HBox(3);
|
||||
bottom.setRight(hBox);
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@ -432,24 +402,24 @@ public final class LogWindow extends Stage {
|
||||
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
||||
|
||||
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
||||
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
|
||||
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
|
||||
|
||||
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
||||
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
|
||||
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
|
||||
|
||||
JFXButton exportDumpButton = new JFXButton();
|
||||
SpinnerPane exportDumpPane = new SpinnerPane();
|
||||
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
|
||||
if (SystemUtils.supportJVMAttachment()) {
|
||||
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
|
||||
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
|
||||
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
|
||||
} else {
|
||||
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
|
||||
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
|
||||
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
|
||||
exportDumpButton.setDisable(true);
|
||||
}
|
||||
exportDumpPane.setContent(exportDumpButton);
|
||||
|
||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
|
||||
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
|
||||
vbox.getChildren().add(bottom);
|
||||
}
|
||||
|
@ -728,10 +728,8 @@ logwindow.title=Log
|
||||
logwindow.help=You can go to the HMCL community and find others for help
|
||||
logwindow.autoscroll=Auto-scroll
|
||||
logwindow.export_game_crash_logs=Export Crash Logs
|
||||
logwindow.export_dump.dependency_ok.button=Export Game Stack Dump
|
||||
logwindow.export_dump.dependency_ok.doing_button=Exporting Game Stack Dump (May take up to 15 seconds)
|
||||
logwindow.export_dump.no_dependency.button=Export Game Stack Dump (Not compatible)
|
||||
logwindow.export_dump.no_dependency.tooltip=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL QQ group or HMCL Discord for help.
|
||||
logwindow.export_dump=Export Game Stack Dump
|
||||
logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL QQ group or HMCL Discord for help.
|
||||
|
||||
main_page=Home
|
||||
|
||||
|
@ -673,10 +673,8 @@ logwindow.title=Registro
|
||||
logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar
|
||||
logwindow.autoscroll=Desplazamiento automático
|
||||
logwindow.export_game_crash_logs=Exportar registros de errores
|
||||
logwindow.export_dump.dependency_ok.button=Exportar volcado de pila de juegos
|
||||
logwindow.export_dump.dependency_ok.doing_button=Exportación de volcado de pila de juego (puede tardar hasta 15 segundos)
|
||||
logwindow.export_dump.no_dependency.button=Exportar volcado de pila de juegos (no compatible)
|
||||
logwindow.export_dump.no_dependency.tooltip=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL QQ o HMCL Discord para obtener ayuda.
|
||||
logwindow.export_dump=Exportar volcado de pila de juegos
|
||||
logwindow.export_dump.no_dependency=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL QQ o HMCL Discord para obtener ayuda.
|
||||
|
||||
|
||||
main_page=Inicio
|
||||
|
@ -607,10 +607,8 @@ logwindow.title=記錄
|
||||
logwindow.help=你可以前往 HMCL 社區,尋找他人幫助
|
||||
logwindow.autoscroll=自動滾動
|
||||
logwindow.export_game_crash_logs=導出遊戲崩潰訊息
|
||||
logwindow.export_dump.dependency_ok.button=導出遊戲運行棧
|
||||
logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒)
|
||||
logwindow.export_dump.no_dependency.button=導出遊戲運行棧(不兼容)
|
||||
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。
|
||||
logwindow.export_dump=導出遊戲運行棧
|
||||
logwindow.export_dump.no_dependency=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。
|
||||
|
||||
main_page=首頁
|
||||
|
||||
|
@ -606,10 +606,8 @@ logwindow.title=日志
|
||||
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
|
||||
logwindow.autoscroll=自动滚动
|
||||
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||
logwindow.export_dump.dependency_ok.button=导出游戏运行栈
|
||||
logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒)
|
||||
logwindow.export_dump.no_dependency.button=导出游戏运行栈(不兼容)
|
||||
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。
|
||||
logwindow.export_dump=导出游戏运行栈
|
||||
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。
|
||||
|
||||
main_page=主页
|
||||
|
||||
|
@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui;
|
||||
import org.jackhuang.hmcl.JavaFXLauncher;
|
||||
import org.jackhuang.hmcl.game.ClassicVersion;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
@ -35,8 +35,6 @@ import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class GameCrashWindowTest {
|
||||
|
||||
@Test
|
||||
@ -57,7 +55,7 @@ public class GameCrashWindowTest {
|
||||
.setGameDir(new File("."))
|
||||
.create(),
|
||||
Arrays.stream(logs.split("\\n"))
|
||||
.map(log -> pair(log, Log4jLevel.guessLevel(log)))
|
||||
.map(Log::new)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
window.showAndWait();
|
||||
|
@ -346,9 +346,15 @@ public final class StringUtils {
|
||||
}
|
||||
|
||||
public static String parseEscapeSequence(String str) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int idx = str.indexOf('\033');
|
||||
if (idx < 0)
|
||||
return str;
|
||||
|
||||
StringBuilder builder = new StringBuilder(str.length());
|
||||
boolean inEscape = false;
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
|
||||
builder.append(str, 0, idx);
|
||||
for (int i = idx; i < str.length(); i++) {
|
||||
char ch = str.charAt(i);
|
||||
if (ch == '\033') {
|
||||
inEscape = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user