Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

825 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Media;
using PCL.Core.App;
using PCL.Core.Logging;
using PCL.Core.App.Localization;
namespace PCL;
public static class ModWatcher
{
// 对全体的监视
public static List<Watcher> mcWatcherList = new();
private static bool isWatcherRunning;
public static bool hasRunningMinecraft;
private static void WatcherStateChanged()
{
var isRunning = false;
var triggerLauncherShutdown = true;
foreach (var Watcher in mcWatcherList)
{
if (Watcher.State == Watcher.MinecraftState.Loading || Watcher.State == Watcher.MinecraftState.Running)
{
isRunning = true;
break;
}
if (Watcher.State == Watcher.MinecraftState.Crashed || Watcher.State == Watcher.MinecraftState.Canceled)
triggerLauncherShutdown = false;
}
if (isWatcherRunning == isRunning)
return;
isWatcherRunning = isRunning;
if (isWatcherRunning)
MinecraftStart();
else
MinecraftStop(triggerLauncherShutdown);
}
private static void MinecraftStart()
{
ModLaunch.McLaunchLog("[全局] 出现运行中的 Minecraft");
hasRunningMinecraft = true;
ModMain.frmMain.BtnExtraShutdown.ShowRefresh();
}
private static void MinecraftStop(bool triggerLauncherShutdown)
{
ModLaunch.McLaunchLog("[全局] 已无运行中的 Minecraft");
hasRunningMinecraft = false;
ModMain.frmMain.BtnExtraShutdown.ShowRefresh();
// 音乐播放
if (Config.Preference.Music.StopInGame)
ModBase.RunInUi(() =>
{
if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在结束后开始音乐播放");
});
else if (Config.Preference.Music.StartInGame)
ModBase.RunInUi(() =>
{
if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在结束后暂停音乐播放");
});
// 开始视频背景播放
ModVideoBack.IsGaming = false;
ModVideoBack.VideoPlay();
// 启动器可见性
switch (Config.Launch.LauncherVisibility)
{
case LauncherVisibility.HideAndExit:
// 直接关闭
if (triggerLauncherShutdown)
ModBase.RunInUi(() => ModMain.frmMain.EndProgram(false));
else
ModBase.RunInUi(() => ModMain.frmMain.Hidden = false);
break;
case LauncherVisibility.HideAndReopen:
// 恢复
ModBase.RunInUi(() => ModMain.frmMain.Hidden = false);
break;
}
}
private static GameLogLevel GetLevel(string line, GameLogLevel lastLevel)
{
Func<string, SolidColorBrush> getColorBrush =
name => (SolidColorBrush)System.Windows.Application.Current.Resources[name];
var starting = line.Split(": ")[0];
if (starting.ContainsF("FATAL"))
return GameLogLevel.Fatal;
if (starting.ContainsF("ERROR"))
return GameLogLevel.Error;
if (starting.ContainsF("WARN"))
return GameLogLevel.Warn;
if (starting.ContainsF("INFO"))
return GameLogLevel.Info;
if (starting.ContainsF("DEBUG"))
return GameLogLevel.Debug;
if (line.StartsWithF("Exception in thread \""))
return GameLogLevel.Error;
if ((line.ContainsF("Exception") || line.ContainsF("Realms authentication error with message ")) &&
lastLevel >= GameLogLevel.Warn)
return lastLevel;
if (line.StartsWithF(" at ") && lastLevel >= GameLogLevel.Warn)
return lastLevel;
return GameLogLevel.Info;
}
private static SolidColorBrush GetColor(GameLogLevel level)
{
Func<string, SolidColorBrush> getColorBrush =
name => (SolidColorBrush)System.Windows.Application.Current.Resources[name];
switch (level)
{
case GameLogLevel.Debug:
{
return getColorBrush("ColorBrushDebug");
}
case GameLogLevel.Info:
{
getColorBrush(ThemeManager.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo");
break;
}
case GameLogLevel.Warn:
{
return getColorBrush("ColorBrushWarn");
}
case GameLogLevel.Error:
{
return getColorBrush("ColorBrushError");
}
case GameLogLevel.Fatal:
{
return getColorBrush("ColorBrushFatal");
}
}
return getColorBrush(ThemeManager.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo");
}
// 实时日志处理
public class LogOutputEventArgs : EventArgs
{
public SolidColorBrush color;
public string logText;
public LogOutputEventArgs(string logText, SolidColorBrush color)
{
this.logText = logText;
this.color = color;
}
}
private enum GameLogLevel
{
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Fatal = 4
}
// 对单个进程的监视
public class Watcher
{
public delegate void GameExitEventHandler();
public delegate void LogOutputEventHandler(Watcher sender, LogOutputEventArgs e);
public enum MinecraftState
{
Loading,
Running,
Crashed,
Ended,
Canceled
}
private readonly int pid;
/// <summary>
/// 是否处理实时日志。
/// </summary>
private readonly bool realTime;
private readonly object waitingLogLock = new();
public uint countDebug;
public uint countError;
public uint countFatal;
public uint countInfo;
public uint countWarn;
/// <summary>
/// 游戏的所有日志输出,只有处理实时日志的情况下才会记录。
/// </summary>
public List<string> fullLog = new();
// 初始化
public Process gameProcess;
// 窗口检查
private bool isWindowAppeared;
/// <summary>
/// 窗口检查是否已经完成。这不一定代表着找到了窗口(如果没有找到,IsWindowAppeared 仍为 False)。
/// </summary>
private bool isWindowFinished;
public string jStackPath;
/// <summary>
/// 上一行日志级别。
/// </summary>
private GameLogLevel lastLevel = GameLogLevel.Info;
public Queue<string> latestLog = new();
public ModLoader.LoaderTask<Process, int> loader;
// 进度更新
private int logProgress;
public McInstance version;
// 日志
public List<string> waitingLog = new(1000);
private nint windowHandle;
private string windowTitle = "";
public Watcher(ModLoader.LoaderTask<Process, int> loader, McInstance version, string windowTitle,
string jStackPath, bool outputRealTime = false)
{
this.loader = loader;
this.version = version;
this.windowTitle = windowTitle;
realTime = outputRealTime;
pid = loader.input.Id;
this.jStackPath = jStackPath;
WatcherLog(Lang.Text("Watcher.Start"));
if (string.IsNullOrWhiteSpace(windowTitle))
WatcherLog("要求窗口标题:" + windowTitle);
// 更改列表
var newWatcherList = new List<Watcher>();
foreach (var Watch in mcWatcherList)
{
if (Watch.State == MinecraftState.Crashed || Watch.State == MinecraftState.Ended ||
Watch.State == MinecraftState.Canceled)
continue;
newWatcherList.Add(Watch);
}
newWatcherList.Add(this);
mcWatcherList = newWatcherList;
WatcherStateChanged();
// 初始化进程与日志读取
gameProcess = loader.input;
gameProcess.BeginOutputReadLine();
gameProcess.BeginErrorReadLine();
gameProcess.OutputDataReceived += LogReceived;
gameProcess.ErrorDataReceived += LogReceived;
// 初始化时钟
// 设置窗口标题
ModBase.RunInNewThread(() =>
{
try
{
while (State != MinecraftState.Ended && State != MinecraftState.Crashed &&
State != MinecraftState.Canceled && loader.State != ModBase.LoadState.Aborted)
{
TimerWindow();
TimerLog();
if (!string.IsNullOrWhiteSpace(windowTitle))
for (var i = 1; i <= 3; i++)
{
if (State == MinecraftState.Running && !gameProcess.HasExited)
{
var realTitle = windowTitle.Replace("{date}", Lang.Date(DateTime.Now, "d"))
.Replace("{time}", Lang.Date(DateTime.Now, "T"));
SetWindowText(windowHandle, realTitle);
}
Thread.Sleep(64);
}
Thread.Sleep(10);
}
WatcherLog(Lang.Text("Watcher.Exited"));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"Minecraft 日志监控主循环出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
State = MinecraftState.Ended;
}
}, "Minecraft Watcher PID " + pid);
}
public MinecraftState State
{
get => field;
set
{
if (field == value)
return;
field = value;
WatcherStateChanged();
}
} = MinecraftState.Loading;
/// <summary>
/// 是否处理实时日志。
/// </summary>
public bool RealTimeLog => realTime;
// 状态
/// <summary>
/// 游戏退出时触发。
/// </summary>
public event GameExitEventHandler? GameExit;
private void LogReceived(object sender, DataReceivedEventArgs e)
{
lock (waitingLogLock)
{
waitingLog.Add(e.Data);
}
if (realTime)
{
LogRealTime(e.Data, ref lastLevel);
if (e.Data is not null)
fullLog.Add(e.Data);
}
}
/// <summary>
/// 触发日志改变事件,并统计日志行数。
/// </summary>
private void LogRealTime(string line, ref GameLogLevel level)
{
if (line is null)
return; // 杀游戏进程时有概率传 null
level = line.StartsWithF(" at ") || line.StartsWithF("Caused by: ") || line.StartsWithF(" ... ")
? level
: GetLevel(line, level);
// “ ... 4 more”
var color = GetColor(level);
switch (level)
{
case GameLogLevel.Debug:
{
countDebug = (uint)(countDebug + 1L);
break;
}
case GameLogLevel.Info:
{
countInfo = (uint)(countInfo + 1L);
break;
}
case GameLogLevel.Warn:
{
countWarn = (uint)(countWarn + 1L);
break;
}
case GameLogLevel.Error:
{
countError = (uint)(countError + 1L);
break;
}
case GameLogLevel.Fatal:
{
countFatal = (uint)(countFatal + 1L);
break;
}
}
LogOutput?.Invoke(this, new LogOutputEventArgs(line, color));
}
/// <summary>
/// 有新的日志输出,日志计数器发生改变时触发。
/// </summary>
public event LogOutputEventHandler? LogOutput;
private void TimerLog()
{
try
{
// 输出文本
var copyed = new List<string>();
lock (waitingLogLock)
{
if (!waitingLog.Any())
return;
copyed = waitingLog;
waitingLog = new List<string>(1000);
}
foreach (var Str in copyed)
GameLog(Str);
if (State == MinecraftState.Loading)
ProgressUpdate();
// 游戏退出检查
if (gameProcess.HasExited)
{
WatcherLog(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode));
// 实时日志输出
if (realTime)
{
var arglevel = GameLogLevel.Info;
LogRealTime(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode), ref arglevel);
}
GameExit?.Invoke();
// If Process.ExitCode = 1 Then
// '返回值为 1,考虑是任务管理器结束
// WatcherLog("Minecraft 返回值为 1,考虑为任务管理器结束") '并不,崩了照样是 1
// State = MinecraftState.Ended
// Else
if (State == MinecraftState.Loading)
{
// 窗口未出现
WatcherLog(Lang.Text("Watcher.Crash.Suspected"));
Crashed();
}
else if (gameProcess.ExitCode != 0 && State == MinecraftState.Running &&
version.releaseTime.Year >= 2012)
{
// 返回值不为 0 且未结束
WatcherLog(Lang.Text("Watcher.Crash.AbnormalExit"));
Crashed();
}
else if (State != MinecraftState.Crashed)
{
// 正常关闭
State = MinecraftState.Ended;
}
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"输出 Minecraft 日志失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
}
}
private void GameLog(string text)
{
// 预处理
if (text is null)
return;
text = text.Replace("\r\n", "\r").Replace("\n", "\r")
.Replace("\r", "\r\n");
// If Text.Contains("") Then Hint("检测到错误的日志编码:" & Text)
// 加入预存储
latestLog.Enqueue(text);
if (latestLog.Count >= 501)
latestLog.Dequeue();
// 进度处理
if (logProgress < 1)
{
WatcherLog(Lang.Text("Watcher.Progress.LogAppeared"));
logProgress = 1;
} // 可能第一句就是后面需要判断的 Log(重现:启动 1.15.2 原版)
if (logProgress < 2 && text.Contains("Setting user:"))
{
WatcherLog(Lang.Text("Watcher.Progress.UserSet")); // 仅确保支持 Minecraft 1.7+
logProgress = 2;
}
else if (logProgress < 3 && text.ContainsF("lwjgl version", true))
{
WatcherLog(Lang.Text("Watcher.Progress.LwjglConfirmed"));
logProgress = 3;
}
else if (logProgress < 4 &&
(text.Contains("OpenAL initialized") || text.Contains("Starting up SoundSystem")))
{
WatcherLog(Lang.Text("Watcher.Progress.OpenAlLoaded")); // 仅确保支持 Minecraft 1.7+
logProgress = 4;
}
else if (logProgress < 5 &&
((text.Contains("Created") && text.Contains("textures") && text.Contains("-atlas")) ||
text.Contains("Found animation info")))
{
WatcherLog(Lang.Text("Watcher.Progress.TexturesLoaded")); // 仅确保支持 Minecraft 1.7+
logProgress = 5;
}
// 输出日志
// Log(Text)
// 关闭与崩溃检测
if (!text.Contains("[CHAT]"))
{
if (text.Contains("Someone is closing me!") ||
text.Contains("Restarting Minecraft with command")) // #1258
{
WatcherLog(Lang.Text("Watcher.Log.CloseDetected", text));
State = MinecraftState.Ended;
}
else if (text.Contains("Crash report saved to") ||
text.Contains("This crash report has been saved to:"))
{
// Text.Contains("Minecraft ran into a problem! Report saved to:") Then
// Minecraft 崩溃,忽略 VanillaFix
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
Crashed();
}
else if (text.Contains("Could not save crash report to"))
{
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
Crashed();
}
else if (text.Contains("/ERROR]: Unable to launch") ||
text.Contains("An exception was thrown, the game will display an error screen and halt."))
{
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
Crashed();
}
}
}
private void WatcherLog(string text)
{
ModLaunch.McLaunchLog("[" + pid + "] " + text);
}
private void ProgressUpdate()
{
double currentProgress;
if (isWindowAppeared || logProgress >= 4)
{
currentProgress = 0.95d;
WatcherLog(Lang.Text("Watcher.LoadComplete"));
State = MinecraftState.Running;
}
else
{
currentProgress = Math.Min(logProgress, 3) / 3d * 0.9d;
}
loader.Progress = currentProgress;
}
private void TimerWindow()
{
try
{
if (gameProcess.HasExited)
return;
if (isWindowFinished)
return;
// 获取全部窗口,检查是否有新增的
KeyValuePair<nint, string>? minecraftWindow = default;
try
{
minecraftWindow = TryGetMinecraftWindow();
}
catch (Win32Exception ex)
{
// 拒绝访问(#1062
ModBase.Log(
ex,
Lang.Text("Watcher.SecurityBlocked"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Watcher.SecurityBlocked"));
isWindowFinished = true;
}
if (minecraftWindow is null)
return;
var minecraftWindowName = minecraftWindow.Value.Value;
var minecraftWindowHandle = minecraftWindow.Value.Key;
// 已找到窗口
if (!minecraftWindowName.StartsWithF("FML") && !minecraftWindowName.StartsWithF("Quilt Loader"))
{
// 已找到 Minecraft 窗口
windowHandle = minecraftWindowHandle;
WatcherLog(Lang.Text("Watcher.WindowLoaded", minecraftWindowName, minecraftWindowHandle.ToInt64()));
isWindowFinished = true;
// 最大化
if (Config.Launch.GameWindowMode == GameWindowSizeMode.Maximized)
// 如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
// ……虽然我很想这样说,但总有人反馈,算了
ModBase.RunInNewThread(() =>
{
try
{
Thread.Sleep(2000);
ShowWindow(windowHandle, 3U);
WatcherLog(Lang.Text("Watcher.WindowMaximized", minecraftWindowHandle.ToInt64()));
}
catch (Exception ex)
{
ModBase.Log(ex, "最大化 Minecraft 窗口时出现错误");
}
}, "MinecraftWindowMaximize");
}
else if (!isWindowAppeared)
{
// 已找到 FML 窗口
WatcherLog(Lang.Text("Watcher.FmlWindowLoaded", minecraftWindowName, minecraftWindowHandle.ToInt64()));
}
isWindowAppeared = true;
}
catch (Exception ex)
{
ModBase.Log(
ex,
"检查 Minecraft 窗口失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
}
}
/// <summary>
/// 获取可能是当前进程对应的 Minecraft 窗口的句柄和标题。
/// Nothing 代表未找到。
/// </summary>
private KeyValuePair<nint, string>? TryGetMinecraftWindow()
{
KeyValuePair<nint, string>? tryGetMinecraftWindowRet = default;
tryGetMinecraftWindowRet = default;
EnumWindows((hwnd, lParam) =>
{
if (tryGetMinecraftWindowRet is not null)
return false; // 找到后停止枚举
var str = new StringBuilder(512);
GetClassName(hwnd, str, str.Capacity);
var className = str.ToString();
if (!(className == "GLFW30" || className == "LWJGL" || className == "SunAwtFrame"))
return true;
// 获取窗口标题名
str = new StringBuilder(512);
GetWindowText(hwnd, str, str.Capacity);
var windowText = str.ToString();
// 部分版本会搞个 GLFW message window 出来所以得反选
if (!(windowText.StartsWithF("FML") ||
(windowText != "PopupMessageWindow" && !windowText.StartsWithF("GLFW"))))
return true;
// 获取窗口关联的进程
var processId = default(int);
GetWindowThreadProcessId(hwnd, ref processId);
try
{
if (processId != gameProcess.Id)
return true;
}
catch (Exception ex)
{
return true;
}
// 找到目标,赋值并停止枚举
tryGetMinecraftWindowRet = new KeyValuePair<nint, string>(hwnd, windowText);
return false;
}, nint.Zero);
return tryGetMinecraftWindowRet;
}
[DllImport("user32")]
private static extern bool EnumWindows(EnumWindowsSub lpEnumFunc, nint lParam);
[DllImport("user32", EntryPoint = "GetClassNameW", CharSet = CharSet.Unicode)]
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32", EntryPoint = "GetWindowTextW", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32", EntryPoint = "SetWindowTextW", CharSet = CharSet.Unicode)]
private static extern bool SetWindowText(nint hWnd, string lpString);
[DllImport("user32")]
private static extern bool ShowWindow(nint hWnd, uint cmdWindow);
[DllImport("user32")]
private static extern int GetWindowThreadProcessId(nint hWnd, ref int lpdwProcessId);
// 崩溃处理
private void Crashed()
{
if (State is MinecraftState.Crashed or MinecraftState.Ended)
return;
State = MinecraftState.Crashed;
// 崩溃分析
if (!Config.Launch.DisableCrashAnalysis)
{
WatcherLog(Lang.Text("Watcher.Crash.Detected"));
HintService.Hint(Lang.Text("Watcher.Crash.Hint"));
ModBase.FeedbackInfo();
ModBase.RunInNewThread(() =>
{
try
{
Thread.Sleep(2000);
WatcherLog(Lang.Text("Watcher.Crash.AnalysisStart"));
var analyzer = new CrashAnalyzer(pid);
analyzer.Collect(version.PathIndie, latestLog.ToList());
analyzer.Prepare();
analyzer.Analyze(version);
analyzer.Output(
false,
[
version.PathInstance + version.Name + ".json",
LogWrapper.CurrentLogger.CurrentLogFiles.Last(),
ModBase.exePath + @"PCL\LatestLaunch.bat"
]);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"崩溃分析失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Crash.Analysis.Error.Failed"));
}
}, "Crash Analyzer");
}
else
{
WatcherLog(Lang.Text("Watcher.Crash.DetectedDisabled"));
}
}
// 强制关闭
public bool CheckAlive(Process p)
{
if (!p.HasExited)
return true;
var exists = Array.Exists(Process.GetProcesses(), item => item.Id == p.Id);
if (exists)
return true;
return false;
}
public void Kill()
{
State = MinecraftState.Canceled;
ModBase.RunInNewThread(() =>
{
WatcherLog(Lang.Text("Watcher.Kill.Attempt"));
try
{
if (CheckAlive(gameProcess))
gameProcess.Kill();
gameProcess.WaitForExit(5000);
if (CheckAlive(gameProcess))
{
WatcherLog(Lang.Text("Watcher.Kill.TaskkillAttempt"));
var taskkillInfo = new ProcessStartInfo
{
FileName = "taskkill.exe",
Arguments = $"/PID {gameProcess.Id} /F /T",
RedirectStandardOutput = true,
UseShellExecute = false
};
var taskkillProcess = Process.Start(taskkillInfo);
var output = taskkillProcess.StandardOutput.ReadToEnd();
WatcherLog(Lang.Text("Watcher.Kill.TaskkillResult", output));
gameProcess.WaitForExit(5000);
if (CheckAlive(gameProcess))
{
WatcherLog(Lang.Text("Watcher.Kill.Timeout"));
return;
}
}
WatcherLog(Lang.Text("Watcher.Kill.Success"));
if (realTime)
{
var arglevel = GameLogLevel.Info;
LogRealTime(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode), ref arglevel);
}
GameExit?.Invoke();
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Watcher.Kill.Failed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Watcher.Kill.Failed"));
}
});
}
// 导出运行栈
public List<string> ExportStackDump(string savePath)
{
var dump = new List<string>();
for (var i = 1; i <= 3; i++)
{
dump.Add(ModBase.ShellAndGetOutput(jStackPath, "-l -e " + gameProcess.Id));
Thread.Sleep(3000);
}
return dump;
}
private delegate bool EnumWindowsSub(nint hwnd, nint lParam);
}
}