feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

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

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

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,24 @@
namespace PCL;
internal sealed class CrashAnalysisContext(
int processId,
string tempFolder)
{
public int ProcessId { get; } = processId;
public string TempFolder { get; } = tempFolder;
public List<CrashLogEntry> RawFiles { get; } = [];
public List<string> OutputFiles { get; } = [];
public McInstance? Instance { get; set; }
public CrashLogEntry? DirectOpenFile { get; set; }
public CrashLogSet? PreparedLogs { get; set; }
public CrashAnalysisResult? Result { get; set; }
public string LogAll { get; set; } = string.Empty;
}
@@ -0,0 +1,66 @@
namespace PCL;
internal sealed class CrashAnalysisResult
{
private readonly List<CrashFinding> _findings = [];
public IReadOnlyList<CrashFinding> Findings => _findings;
public bool HasFinding => _findings.Count > 0;
public bool Any => HasFinding;
public void Add(CrashFinding finding)
{
var existing = _findings.FirstOrDefault(item => item.Cause == finding.Cause);
if (existing is null)
{
_findings.Add(finding);
return;
}
foreach (var evidence in finding.Evidence)
existing.AddEvidence(evidence);
existing.ShouldStop |= finding.ShouldStop;
}
public void Add(
CrashCause cause,
CrashConfidence confidence,
IEnumerable<string>? details = null,
CrashLogKind? source = null,
string? pattern = null,
string? displayKind = null,
bool shouldStop = false)
{
Add(new CrashFinding(cause, confidence, ToEvidence(details, source, pattern, displayKind))
{
ShouldStop = shouldStop
});
}
private static IEnumerable<CrashEvidence> ToEvidence(
IEnumerable<string>? details,
CrashLogKind? source,
string? pattern,
string? displayKind)
{
if (details is null)
yield break;
foreach (var detail in details)
{
if (string.IsNullOrWhiteSpace(detail))
continue;
yield return new CrashEvidence
{
Value = detail.Trim(),
Source = source,
Pattern = pattern,
DisplayKind = displayKind
};
}
}
}
@@ -0,0 +1,75 @@
using System.IO;
using PCL.Core.Logging;
namespace PCL;
public sealed class CrashAnalyzer
{
private readonly CrashLogCollector _collector;
private readonly CrashAnalysisContext _context;
private readonly CrashDetector _detector;
private readonly CrashDialogPresenter _dialogPresenter;
private readonly CrashLogImporter _importer;
private readonly CrashLogPreparer _preparer;
public CrashAnalyzer(int uUid)
{
var tempFolder = ModMain.RequestTaskTempFolder();
Directory.CreateDirectory(Path.Combine(tempFolder, "Temp"));
Directory.CreateDirectory(Path.Combine(tempFolder, "Report"));
_context = new CrashAnalysisContext(uUid, tempFolder);
_collector = new CrashLogCollector(_context);
_importer = new CrashLogImporter(_context);
_preparer = new CrashLogPreparer(_context);
_detector = new CrashDetector();
_dialogPresenter = new CrashDialogPresenter(_context);
LogWrapper.Info("Crash", $"崩溃分析暂存文件夹:{tempFolder}");
}
/// <summary>
/// 将可用于分析的日志存储到崩溃分析上下文。
/// </summary>
/// <param name="latestLog">从 PCL 捕获到的最后 200 行程序输出。</param>
public void Collect(string versionPathIndie, IList<string>? latestLog = null)
{
_collector.Collect(versionPathIndie, latestLog);
}
/// <summary>
/// 从文件路径直接导入日志文件或崩溃报告压缩包。
/// </summary>
public void Import(string filePath)
{
_importer.Import(filePath);
}
/// <summary>
/// 从原始日志中提取实际有用的文本片段并整理可用于生成报告的文件。
/// </summary>
public bool Prepare()
{
return _preparer.Prepare();
}
/// <summary>
/// 根据整理后的日志与可能的实例信息分析崩溃原因。
/// </summary>
public void Analyze(McInstance? version = null)
{
_context.Instance = version;
var logs = _context.PreparedLogs ??
throw new InvalidOperationException("Prepare must be called before Analyze.");
_context.Result = _detector.Analyze(logs, version);
_context.LogAll = logs.All;
}
/// <summary>
/// 弹出崩溃弹窗,并指导导出崩溃报告。
/// </summary>
public void Output(bool isHandAnalyze, List<string>? extraFiles = null)
{
_dialogPresenter.Output(isHandAnalyze, extraFiles);
}
}
@@ -0,0 +1,53 @@
namespace PCL;
/// <summary>
/// Stable internal crash cause identifiers.
/// </summary>
internal enum CrashCause
{
ExtractedModFile,
MissingMixinBootstrap,
OutOfMemory,
UsingJdk,
UnsupportedOpenGl,
UsingOpenJ9,
JavaTooNew,
JavaIncompatible,
InvalidModFileName,
PixelFormatNotSupported,
VeryShortOutput,
IntelDriverAccessViolation,
AmdDriverAccessViolation,
NvidiaDriverAccessViolation,
ManualDebugCrash,
OpenGl1282,
FileOrContentValidationFailed,
ConfirmedModCrash,
SuspectedModCrash,
ModConfigCrash,
ModMixinFailed,
ModLoaderError,
ModInitializationFailed,
StackKeywordFound,
StackModNameFound,
OptiFineWorldLoadCrash,
SpecificBlockCrash,
SpecificEntityCrash,
ResourcePackTooLarge,
NoAnalyzableFile,
X86JavaMemoryLimit,
DuplicateMods,
IncompatibleMods,
OptiFineForgeIncompatible,
FabricError,
FabricSolutionProvided,
ForgeError,
OldForgeNewJavaIncompatible,
MultipleForgeInInstanceJson,
TooManyModsIdLimit,
NightConfigBug,
ShadersModWithOptiFine,
IncompleteForgeInstallation,
ModRequiresJava11,
MissingDependencyOrWrongMcVersion
}
@@ -0,0 +1,19 @@
namespace PCL;
internal sealed class CrashEvidence
{
public required string Value { get; init; }
public CrashLogKind? Source { get; init; }
public string? Pattern { get; init; }
public string? DisplayKind { get; init; }
public bool EqualsTo(CrashEvidence other)
{
return Source == other.Source &&
string.Equals(Value, other.Value, StringComparison.Ordinal) &&
string.Equals(DisplayKind, other.DisplayKind, StringComparison.Ordinal);
}
}
@@ -0,0 +1,66 @@
using System.IO;
using System.Text;
using PCL.Core.IO;
using PCL.Core.Logging;
using PCL.Core.Utils.Codecs;
namespace PCL;
internal static class CrashFileIo
{
public static byte[] ReadBytes(string filePath)
{
try
{
// 使用 FileShare.ReadWrite 以读取正在被 Logger 写入的文件
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var ms = new MemoryStream();
fs.CopyTo(ms);
return ms.ToArray();
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash IO", "读取与游戏崩溃关联的日志文件时出错,");
return Files.ReadAllBytesOrEmptyAsync(filePath).GetAwaiter().GetResult();
}
}
public static string ReadText(string filePath, Encoding? encoding = null)
{
var bytes = ReadBytes(filePath);
return encoding is null
? EncodingUtils.DecodeBytes(bytes)
: encoding.GetString(bytes);
}
public static void WriteText(string filePath, string text, Encoding? encoding = null)
{
Files.WriteFileAsync(filePath, text, encoding: encoding).GetAwaiter().GetResult();
}
public static void CopyFile(string fromPath, string toPath)
{
Files.CopyFileAsync(fromPath, toPath).GetAwaiter().GetResult();
}
public static void DeleteDirectory(string directoryPath)
{
Directories.DeleteDirectoryAsync(directoryPath, true).GetAwaiter().GetResult();
}
public static bool CanExtractArchive(string archivePath)
{
var fileName = Path.GetFileName(archivePath);
return fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ||
fileName.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) ||
fileName.EndsWith(".bz2", StringComparison.OrdinalIgnoreCase);
}
public static void ExtractFile(string archivePath, string destinationDirectory)
{
if (!CanExtractArchive(archivePath))
throw new NotSupportedException("崩溃日志导入不支持该压缩格式。支持 zip、gz、bz2、tar、tgz 与 tar.gz。");
Files.ExtractFileAsync(archivePath, destinationDirectory).GetAwaiter().GetResult();
}
}
@@ -0,0 +1,39 @@
namespace PCL;
internal sealed class CrashFinding
{
public CrashFinding(
CrashCause cause,
CrashConfidence confidence,
IEnumerable<CrashEvidence>? evidence = null)
{
Cause = cause;
Confidence = confidence;
Evidence = evidence
?.Where(item => !string.IsNullOrWhiteSpace(item.Value))
.ToList() ?? [];
}
public CrashCause Cause { get; }
public CrashConfidence Confidence { get; }
public List<CrashEvidence> Evidence { get; }
public bool ShouldStop { get; set; }
public IReadOnlyList<string> Details => Evidence
.Select(item => item.Value)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.ToList();
public void AddEvidence(CrashEvidence evidence)
{
if (string.IsNullOrWhiteSpace(evidence.Value))
return;
if (!Evidence.Any(item => item.EqualsTo(evidence)))
Evidence.Add(evidence);
}
}
@@ -0,0 +1,60 @@
using System.Text.RegularExpressions;
using PCL.Core.Logging;
namespace PCL;
internal static class CrashRegex
{
public static string? First(
string text,
string pattern,
RegexOptions options = RegexOptions.None)
{
try
{
var value = Regex.Match(text, pattern, options).Value;
return string.IsNullOrEmpty(value)
? null
: value;
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "正则匹配第一项出错");
return null;
}
}
public static List<string> All(
string text,
string pattern,
RegexOptions options = RegexOptions.None)
{
try
{
return Regex.Matches(text, pattern, options)
.Select(match => match.Value)
.ToList();
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "正则匹配全部项出错");
return [];
}
}
public static bool IsMatch(
string text,
string pattern,
RegexOptions options = RegexOptions.None)
{
try
{
return Regex.IsMatch(text, pattern, options);
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "正则检查出错");
return false;
}
}
}
@@ -0,0 +1,75 @@
namespace PCL;
internal static class CrashText
{
public static string BeforeFirst(
string text,
string marker,
bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(marker)
? -1
: text.IndexOf(marker, _Comparison(ignoreCase));
return pos >= 0
? text[..pos]
: text;
}
public static string AfterFirst(
string text,
string marker,
bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(marker)
? -1
: text.IndexOf(marker, _Comparison(ignoreCase));
return pos >= 0
? text[(pos + marker.Length)..]
: text;
}
public static string AfterLast(
string text,
string marker,
bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(marker)
? -1
: text.LastIndexOf(marker, _Comparison(ignoreCase));
return pos >= 0
? text[(pos + marker.Length)..]
: text;
}
public static string Between(
string text,
string after,
string before,
bool ignoreCase = false)
{
var comparison = _Comparison(ignoreCase);
var startPos = string.IsNullOrEmpty(after)
? -1
: text.LastIndexOf(after, comparison);
startPos = startPos >= 0
? startPos + after.Length
: 0;
var endPos = string.IsNullOrEmpty(before)
? -1
: text.IndexOf(before, startPos, comparison);
if (endPos >= 0)
return text[startPos..endPos];
return startPos > 0
? text[startPos..]
: text;
}
private static StringComparison _Comparison(bool ignoreCase)
{
return ignoreCase
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
}
}
@@ -0,0 +1,121 @@
using PCL.Core.Logging;
namespace PCL;
internal sealed class CrashDetector
{
private readonly CrashEvidenceExtractor _extractor = new();
private readonly CrashStackAnalyzer _stackAnalyzer = new();
public CrashAnalysisResult Analyze(CrashLogSet logs, McInstance? instance)
{
LogWrapper.Info("Crash", "步骤 3:分析崩溃原因");
var result = new CrashAnalysisResult();
if (!logs.HasAnalyzableLog)
{
result.Add(new CrashFinding(CrashCause.NoAnalyzableFile, CrashConfidence.High) { ShouldStop = true });
_LogSummary(result);
return result;
}
var normalizedLogs = _Normalize(logs);
var modIndex = CrashModIndex.Create(normalizedLogs, instance);
_RunPhase(DetectionPhase.Fatal, normalizedLogs, modIndex, result);
if (_ShouldStop(result))
{
_LogSummary(result);
return result;
}
_RunPhase(DetectionPhase.Primary, normalizedLogs, modIndex, result);
if (_ShouldStop(result))
{
_LogSummary(result);
return result;
}
var stackFinding = _stackAnalyzer.Analyze(normalizedLogs, modIndex);
if (stackFinding is not null)
result.Add(stackFinding);
if (_ShouldStop(result))
{
_LogSummary(result);
return result;
}
_RunPhase(DetectionPhase.Secondary, normalizedLogs, modIndex, result);
_LogSummary(result);
return result;
}
private static CrashLogSet _Normalize(CrashLogSet logs)
{
var all = logs.All;
if (all.Contains("quilt", StringComparison.OrdinalIgnoreCase) &&
all.Contains("Mod Table Version", StringComparison.Ordinal))
{
LogWrapper.Info("Crash", "处理 Quilt Mod Table 后再继续分析");
all = CrashText.BeforeFirst(all, "| Index") + CrashText.AfterFirst(all, "Mod Table Version:");
}
return new CrashLogSet
{
Game = logs.Game,
Debug = logs.Debug,
CrashReport = logs.CrashReport,
HsErr = logs.HsErr,
All = all
};
}
private void _RunPhase(
DetectionPhase phase,
CrashLogSet logs,
CrashModIndex modIndex,
CrashAnalysisResult result)
{
var input = new CrashRuleInput { Logs = logs, ModIndex = modIndex };
foreach (var rule in CrashRuleCatalog.Rules.Where(rule => rule.Phase == phase))
{
var finding = rule.Evaluate(input);
if (finding is not null)
_AddAndLog(result, finding);
}
foreach (var finding in _extractor.Extract(phase, logs, modIndex))
_AddAndLog(result, finding);
}
private static void _AddAndLog(CrashAnalysisResult result, CrashFinding finding)
{
result.Add(finding);
_LogFinding(finding);
}
private static bool _ShouldStop(CrashAnalysisResult result)
{
return result.Findings.Any(finding => finding.ShouldStop);
}
private static void _LogFinding(CrashFinding finding)
{
var evidence = string.Join("", finding.Details);
LogWrapper.Info(
"Crash",
$"可能的崩溃原因:{finding.Cause}{(string.IsNullOrEmpty(evidence) ? "" : "" + evidence + "")}");
}
private static void _LogSummary(CrashAnalysisResult result)
{
if (!result.Any)
{
LogWrapper.Info("Crash", "步骤 3:分析崩溃原因完成,未找到可能的原因");
return;
}
LogWrapper.Info("Crash", $"步骤 3:分析崩溃原因完成,找到 {result.Findings.Count} 条可能的原因");
foreach (var finding in result.Findings)
_LogFinding(finding);
}
}
@@ -0,0 +1,714 @@
using System.Text.RegularExpressions;
namespace PCL;
internal sealed class CrashEvidenceExtractor
{
public IEnumerable<CrashFinding> Extract(
DetectionPhase phase,
CrashLogSet logs,
CrashModIndex modIndex)
{
return phase switch
{
DetectionPhase.Fatal => _ExtractFatal(logs, modIndex),
DetectionPhase.Primary => _ExtractPrimary(logs, modIndex),
DetectionPhase.Secondary => _ExtractSecondary(logs, modIndex),
_ => []
};
}
private static IEnumerable<CrashFinding> _ExtractFatal(
CrashLogSet logs,
CrashModIndex modIndex)
{
foreach (var finding in _ExtractOutOfMemory(logs))
yield return finding;
var memoryReservation = _ExtractJvmMemoryReservation(logs);
if (memoryReservation is not null)
yield return memoryReservation;
foreach (var finding in _ExtractAccessViolation(logs))
yield return finding;
var signer = _ExtractSignerValidationFailure(logs);
if (signer is not null)
yield return signer;
var optifineMissingMods = _ExtractOptiFineMissingMods(logs);
if (optifineMissingMods is not null)
yield return optifineMissingMods;
foreach (var finding in _ExtractConfirmedModCrash(logs, modIndex))
yield return finding;
foreach (var finding in _ExtractDuplicateMods(logs))
yield return finding;
foreach (var finding in _ExtractMissingDependency(logs))
yield return finding;
}
private static IEnumerable<CrashFinding> _ExtractPrimary(
CrashLogSet logs,
CrashModIndex modIndex)
{
foreach (var finding in _ExtractMixinFailure(logs, modIndex))
yield return finding;
foreach (var finding in _ExtractForgeError(logs))
yield return finding;
foreach (var finding in _ExtractFabricSolution(logs))
yield return finding;
foreach (var finding in _ExtractFabricProvidedModCrash(logs, modIndex))
yield return finding;
foreach (var finding in _ExtractSuspectedMods(logs, modIndex))
yield return finding;
}
private static IEnumerable<CrashFinding> _ExtractSecondary(
CrashLogSet logs,
CrashModIndex modIndex)
{
var shortOutput = _ExtractShortOutput(logs);
if (shortOutput is not null)
yield return shortOutput;
var modLoader = _ExtractModLoaderError(logs);
if (modLoader is not null)
yield return modLoader;
var modInitialization = _ExtractModInitializationFailure(logs, modIndex);
if (modInitialization is not null)
yield return modInitialization;
foreach (var finding in _ExtractSpecificBlockAndEntity(logs))
yield return finding;
}
private static IEnumerable<CrashFinding> _ExtractOutOfMemory(CrashLogSet logs)
{
var evidence = _FindPatterns(
logs,
[
(CrashLogKind.Game, "java.lang.OutOfMemoryError"),
(CrashLogKind.Game, "an out of memory error"),
(CrashLogKind.HsErr, "The system is out of physical RAM or swap space"),
(CrashLogKind.HsErr, "Out of Memory Error"),
(CrashLogKind.CrashReport, "java.lang.OutOfMemoryError")
])
.ToList();
if (evidence.Count == 0)
yield break;
yield return new CrashFinding(
CrashCause.OutOfMemory,
CrashConfidence.High,
evidence)
{
ShouldStop = true
};
}
private static CrashFinding? _ExtractJvmMemoryReservation(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
!gameLog.Contains("Could not reserve enough space", StringComparison.Ordinal))
return null;
var cause = gameLog.Contains("for 1048576KB object heap", StringComparison.Ordinal)
? CrashCause.X86JavaMemoryLimit
: CrashCause.OutOfMemory;
return new CrashFinding(
cause,
CrashConfidence.High,
[
_Evidence(
"Could not reserve enough space",
CrashLogKind.Game,
"jvm-message")
])
{
ShouldStop = true
};
}
private static IEnumerable<CrashFinding> _ExtractAccessViolation(CrashLogSet logs)
{
var hsLog = logs.HsErr?.Text;
if (string.IsNullOrEmpty(hsLog) ||
!hsLog.Contains("EXCEPTION_ACCESS_VIOLATION", StringComparison.Ordinal))
yield break;
if (hsLog.Contains("# C [ig", StringComparison.Ordinal))
yield return _DriverFinding(CrashCause.IntelDriverAccessViolation, "# C [ig");
if (hsLog.Contains("# C [atio", StringComparison.Ordinal))
yield return _DriverFinding(CrashCause.AmdDriverAccessViolation, "# C [atio");
if (hsLog.Contains("# C [nvoglv", StringComparison.Ordinal))
yield return _DriverFinding(CrashCause.NvidiaDriverAccessViolation, "# C [nvoglv");
}
private static CrashFinding _DriverFinding(
CrashCause cause,
string pattern)
{
return new CrashFinding(
cause,
CrashConfidence.High,
[_Evidence(pattern, CrashLogKind.HsErr, "driver-signature")])
{
ShouldStop = true
};
}
private static CrashFinding? _ExtractSignerValidationFailure(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
!gameLog.Contains(
"signer information does not match signer information of other classes in the same package",
StringComparison.Ordinal))
return null;
var detail = (CrashRegex.First(gameLog, "(?<=class \")[^']+(?=\"'s signer information)") ?? "")
.TrimEnd('\r', '\n');
return new CrashFinding(
CrashCause.FileOrContentValidationFailed,
CrashConfidence.High,
[_Evidence(detail, CrashLogKind.Game, "class")])
{
ShouldStop = true
};
}
private static CrashFinding? _ExtractOptiFineMissingMods(CrashLogSet logs)
{
var crash = logs.CrashReport?.Text;
if (string.IsNullOrEmpty(crash) ||
!crash.Contains("has mods that were not found", StringComparison.Ordinal) ||
!CrashRegex.IsMatch(crash, @"The Mod File [^\n]+optifine\\OptiFine[^\n]+ has mods that were not found"))
return null;
return new CrashFinding(
CrashCause.OptiFineForgeIncompatible,
CrashConfidence.High,
[_Evidence("OptiFine", CrashLogKind.CrashReport, "mod")])
{
ShouldStop = true
};
}
private static IEnumerable<CrashFinding> _ExtractConfirmedModCrash(
CrashLogSet logs,
CrashModIndex modIndex)
{
var gameLog = logs.Game?.Text;
if (!string.IsNullOrEmpty(gameLog) &&
gameLog.Contains("Caught exception from ", StringComparison.Ordinal))
{
var hint = CrashRegex.First(gameLog, @"(?<=Caught exception from )[^\n]+?")
?.TrimEnd('\r', '\n', ' ');
yield return _ModFinding(
CrashCause.ConfirmedModCrash,
modIndex.ResolveToDisplayNames([hint ?? ""]),
CrashLogKind.Game,
true);
}
var crash = logs.CrashReport?.Text;
if (string.IsNullOrEmpty(crash))
yield break;
if (crash.Contains("-- MOD ", StringComparison.Ordinal))
{
var modSection = CrashText.Between(crash, "-- MOD ", "Failure message:");
if (modSection.Contains(".jar", StringComparison.OrdinalIgnoreCase))
{
var fileName = (CrashRegex.First(modSection, "(?<=Mod File: ).+") ?? "")
.TrimEnd('\r', '\n', ' ');
yield return _ModFinding(
CrashCause.ConfirmedModCrash,
[fileName],
CrashLogKind.CrashReport,
true);
}
else
{
var message = (CrashRegex.First(crash, @"(?<=Failure message: )[\w\W]+?(?=\tMod)") ?? "")
.Replace("\t", " ")
.TrimEnd('\r', '\n', ' ');
yield return _ModFinding(
CrashCause.ModLoaderError,
[message],
CrashLogKind.CrashReport,
true,
"loader-message");
}
}
if (crash.Contains("Multiple entries with same key: ", StringComparison.Ordinal))
{
var hint = (CrashRegex.First(crash, "(?<=Multiple entries with same key: )[^=]+") ?? "")
.TrimEnd('\r', '\n', ' ');
yield return _ModFinding(
CrashCause.ConfirmedModCrash,
modIndex.ResolveToDisplayNames([hint]),
CrashLogKind.CrashReport,
true);
}
if (crash.Contains("LoaderExceptionModCrash: Caught exception from ", StringComparison.Ordinal))
{
var hint = (CrashRegex.First(crash, @"(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+") ?? "")
.TrimEnd('\r', '\n', ' ');
yield return _ModFinding(
CrashCause.ConfirmedModCrash,
modIndex.ResolveToDisplayNames([hint]),
CrashLogKind.CrashReport,
true);
}
if (crash.Contains("Failed loading config file ", StringComparison.Ordinal))
{
var mod = (CrashRegex.First(crash, @"(?<=Failed loading config file .+ for modid )[^\n]+") ?? "")
.TrimEnd('\r', '\n');
var config = (CrashRegex.First(crash, "(?<=Failed loading config file ).+(?= of type)") ?? "")
.TrimEnd('\r', '\n');
var resolved = modIndex.ResolveToDisplayNames([mod]);
var evidence = new List<CrashEvidence>();
evidence.AddRange(resolved.Select(item => _Evidence(item, CrashLogKind.CrashReport, "mod")));
if (!string.IsNullOrWhiteSpace(config))
evidence.Add(_Evidence(config, CrashLogKind.CrashReport, "config"));
yield return new CrashFinding(
CrashCause.ModConfigCrash,
CrashConfidence.High,
evidence)
{
ShouldStop = true
};
}
}
private static IEnumerable<CrashFinding> _ExtractDuplicateMods(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog))
yield break;
if (gameLog.Contains("DuplicateModsFoundException", StringComparison.Ordinal))
yield return _ModFinding(
CrashCause.DuplicateMods,
CrashRegex.All(
gameLog,
@"(?<=\n\t[\w]+ : [A-Z]:[^\n]+(/|\\))[^/\\\n]+?.jar",
RegexOptions.IgnoreCase),
CrashLogKind.Game,
true);
if (gameLog.Contains("Found a duplicate mod", StringComparison.Ordinal))
yield return _ModFinding(
CrashCause.DuplicateMods,
CrashRegex.All(
CrashRegex.First(gameLog, @"Found a duplicate mod[^\n]+") ?? "",
@"[^\\/]+.jar",
RegexOptions.IgnoreCase),
CrashLogKind.Game,
true);
if (gameLog.Contains("Found duplicate mods", StringComparison.Ordinal))
yield return _ModFinding(
CrashCause.DuplicateMods,
CrashRegex.All(gameLog, @"(?<=Mod ID: ')\w+?(?=' from mod files:)")
.Distinct()
.ToList(),
CrashLogKind.Game,
true);
if (gameLog.Contains("ModResolutionException: Duplicate", StringComparison.Ordinal))
yield return _ModFinding(
CrashCause.DuplicateMods,
CrashRegex.All(
CrashRegex.First(gameLog, @"ModResolutionException: Duplicate[^\n]+") ?? "",
@"[^\\/]+.jar",
RegexOptions.IgnoreCase),
CrashLogKind.Game,
true);
}
private static IEnumerable<CrashFinding> _ExtractMissingDependency(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog))
yield break;
if (gameLog.Contains("Incompatible mods found!", StringComparison.Ordinal))
yield return _ModFinding(
CrashCause.IncompatibleMods,
[CrashRegex.First(gameLog, @"(?<=Incompatible mods found![\s\S]+: )[\s\S]+?(?=\tat )") ?? ""],
CrashLogKind.Game,
true,
"loader-message");
if (gameLog.Contains("Missing or unsupported mandatory dependencies:", StringComparison.Ordinal))
{
var details = CrashRegex.All(
gameLog,
@"(?<=Missing or unsupported mandatory dependencies:)([\n\r]+\t(.*))+",
RegexOptions.IgnoreCase)
.Select(item => item.Trim('\r', '\n', '\t', ' '))
.Distinct()
.ToList();
yield return _ModFinding(
CrashCause.MissingDependencyOrWrongMcVersion,
details,
CrashLogKind.Game,
true,
"loader-message");
}
}
private static IEnumerable<CrashFinding> _ExtractMixinFailure(
CrashLogSet logs,
CrashModIndex modIndex)
{
return from source in new[] { CrashLogKind.Game, CrashLogKind.CrashReport }
let text = logs.GetText(source)
where !string.IsNullOrEmpty(text) && _LooksLikeMixinFailure(text)
let modHints = _ExtractMixinModHints(text)
.Distinct(StringComparer.Ordinal)
.ToList()
let resolved = modIndex.ResolveToDisplayNames(modHints)
let hasResolved = modHints.Count > 0 && !modHints.SequenceEqual(resolved)
let evidence = (resolved.Count != 0 ? resolved : modHints)
.Select(item => _Evidence(item, source, hasResolved ? "mod" : "mod-hint"))
select new CrashFinding(
CrashCause.ModMixinFailed,
hasResolved ? CrashConfidence.High : CrashConfidence.Medium,
evidence)
{
ShouldStop = true
};
}
private static bool _LooksLikeMixinFailure(string text)
{
return text.Contains("Mixin prepare failed ", StringComparison.Ordinal) ||
text.Contains("Mixin apply failed ", StringComparison.Ordinal) ||
text.Contains("MixinApplyError", StringComparison.Ordinal) ||
text.Contains("MixinTransformerError", StringComparison.Ordinal) ||
text.Contains("mixin.injection.throwables.", StringComparison.Ordinal) ||
text.Contains(".json] FAILED during )", StringComparison.Ordinal);
}
private static IEnumerable<string> _ExtractMixinModHints(string text)
{
var patterns = new[]
{
new Regex(
@"(?<=from mod )[^.\/ ]+(?=\] from)",
RegexOptions.Compiled),
new Regex(
@"(?<=for mod )[^.\/ ]+(?= failed)",
RegexOptions.Compiled),
new Regex(
@"(?<=^[^\t]+[ \[{(]{1})[^ \[{(]+\.[^ ]+(?=\.json)",
RegexOptions.Compiled | RegexOptions.Multiline)
};
foreach (var pattern in patterns)
foreach (Match match in pattern.Matches(text))
{
var value = match.Value
.Replace("mixins", "mixin")
.Replace(".mixin", "")
.Replace("mixin.", "")
.Trim();
if (!string.IsNullOrEmpty(value))
yield return value;
}
}
private static IEnumerable<CrashFinding> _ExtractForgeError(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
!gameLog.Contains(
"An exception was thrown, the game will display an error screen and halt.",
StringComparison.Ordinal))
yield break;
var message = CrashRegex.First(
gameLog,
@"(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")
?.Trim('\r', '\n') ?? "";
yield return _ModFinding(
CrashCause.ForgeError,
[message],
CrashLogKind.Game,
true,
"loader-message");
}
private static IEnumerable<CrashFinding> _ExtractFabricSolution(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog))
yield break;
var solution = "";
if (gameLog.Contains("A potential solution has been determined:", StringComparison.Ordinal))
solution = string.Join(
"\n",
CrashRegex.All(
CrashRegex.First(gameLog, @"(?<=A potential solution has been determined:\n)(\s+ - [^\n]+\n)+") ??
"",
@"(?<=\s+)[^\n]+"));
else if (gameLog.Contains(
"A potential solution has been determined, this may resolve your problem:",
StringComparison.Ordinal))
solution = string.Join(
"\n",
CrashRegex.All(
CrashRegex.First(
gameLog,
@"(?<=A potential solution has been determined, this may resolve your problem:\n)(\s+ - [^\n]+\n)+") ??
"",
@"(?<=\s+)[^\n]+"));
else if (gameLog.Contains("确定了一种可能的解决方法,这样做可能会解决你的问题:", StringComparison.Ordinal))
solution = string.Join(
"\n",
CrashRegex.All(
CrashRegex.First(gameLog, @"(?<=确定了一种可能的解决方法,这样做可能会解决你的问题:\n)(\s+ - [^\n]+\n)+") ?? "",
@"(?<=\s+)[^\n]+"));
if (!string.IsNullOrWhiteSpace(solution))
yield return _ModFinding(
CrashCause.FabricSolutionProvided,
[solution],
CrashLogKind.Game,
true,
"loader-message");
}
private static IEnumerable<CrashFinding> _ExtractFabricProvidedModCrash(
CrashLogSet logs,
CrashModIndex modIndex)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
!gameLog.Contains("due to errors, provided by ", StringComparison.Ordinal) ||
_LooksLikeMixinFailure(gameLog))
yield break;
var hint = (CrashRegex.First(gameLog, "(?<=due to errors, provided by ')[^']+") ?? "")
.TrimEnd('\r', '\n', ' ');
yield return _ModFinding(
CrashCause.ConfirmedModCrash,
modIndex.ResolveToDisplayNames([hint]),
CrashLogKind.Game,
true);
}
private static IEnumerable<CrashFinding> _ExtractSuspectedMods(
CrashLogSet logs,
CrashModIndex modIndex)
{
var crash = logs.CrashReport?.Text;
if (string.IsNullOrEmpty(crash) ||
!crash.Contains("Suspected Mod", StringComparison.Ordinal))
yield break;
var suspectsRaw = CrashText.Between(crash, "Suspected Mod", "Stacktrace");
if (suspectsRaw.StartsWith("s: None", StringComparison.Ordinal))
yield break;
var suspects = CrashRegex.All(suspectsRaw, @"(?<=\n\t[^(\t]+\()[^)\n]+");
if (suspects.Count != 0)
yield return _ModFinding(
CrashCause.SuspectedModCrash,
modIndex.ResolveToDisplayNames(suspects),
CrashLogKind.CrashReport,
true);
}
private static CrashFinding? _ExtractShortOutput(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
logs.HsErr is not null ||
logs.CrashReport is not null ||
gameLog.Contains("at net.", StringComparison.Ordinal) ||
gameLog.Contains("INFO]", StringComparison.Ordinal) ||
gameLog.Length >= 100)
return null;
return _ModFinding(
CrashCause.VeryShortOutput,
[gameLog],
CrashLogKind.Game,
false,
"raw-output");
}
private static CrashFinding? _ExtractModLoaderError(CrashLogSet logs)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
!gameLog.Contains("Mod resolution failed", StringComparison.Ordinal))
return null;
return new CrashFinding(
CrashCause.ModLoaderError,
CrashConfidence.High);
}
private static CrashFinding? _ExtractModInitializationFailure(
CrashLogSet logs,
CrashModIndex modIndex)
{
var gameLog = logs.Game?.Text;
if (string.IsNullOrEmpty(gameLog) ||
!gameLog.Contains("Failed to create mod instance.", StringComparison.Ordinal))
return null;
var hint = (CrashRegex.First(gameLog, "(?<=Failed to create mod instance. ModID: )[^,]+") ??
CrashRegex.First(gameLog, @"(?<=Failed to create mod instance. ModId )[^\n]+(?= for )") ?? "")
.TrimEnd('\r', '\n');
return _ModFinding(
CrashCause.ModInitializationFailed,
modIndex.ResolveToDisplayNames([hint]),
CrashLogKind.Game,
false);
}
private static IEnumerable<CrashFinding> _ExtractSpecificBlockAndEntity(CrashLogSet logs)
{
var crash = logs.CrashReport?.Text;
if (string.IsNullOrEmpty(crash))
yield break;
if (crash.Contains("\tBlock location: World: ", StringComparison.Ordinal))
{
var value =
(CrashRegex.First(crash, @"(?<=\tBlock: Block\{)[^\}]+") ?? "") +
" " +
(CrashRegex.First(crash, @"(?<=\tBlock location: World: )\([^\)]+\)") ?? "");
yield return _ModFinding(
CrashCause.SpecificBlockCrash,
[value],
CrashLogKind.CrashReport,
false,
"block");
}
if (crash.Contains("\tEntity's Exact location: ", StringComparison.Ordinal))
{
var value =
(CrashRegex.First(crash, @"(?<=\tEntity Type: )[^\n]+(?= \()") ?? "") +
" (" +
(CrashRegex.First(crash, @"(?<=\tEntity's Exact location: )[^\n]+") ?? "")
.TrimEnd('\r', '\n') +
")";
yield return _ModFinding(
CrashCause.SpecificEntityCrash,
[value],
CrashLogKind.CrashReport,
false,
"entity");
}
}
private static IEnumerable<CrashEvidence> _FindPatterns(
CrashLogSet logs,
IEnumerable<(CrashLogKind Source, string Pattern)> patterns)
{
foreach (var (source, pattern) in patterns)
{
var text = logs.GetText(source);
if (text?.Contains(pattern, StringComparison.Ordinal) == true)
yield return _Evidence(pattern, source, "pattern", pattern);
}
}
private static CrashFinding _ModFinding(
CrashCause cause,
IEnumerable<string?> values,
CrashLogKind source,
bool shouldStop,
string displayKind = "mod")
{
return new CrashFinding(
cause,
CrashConfidence.High,
values
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => _Evidence(value!, source, displayKind)))
{
ShouldStop = shouldStop
};
}
private static CrashEvidence _Evidence(
string value,
CrashLogKind source,
string displayKind,
string? pattern = null)
{
return new CrashEvidence
{
Value = value,
Source = source,
Pattern = pattern,
DisplayKind = displayKind
};
}
}
@@ -0,0 +1,255 @@
using System.IO;
using System.Text.RegularExpressions;
using PCL.Core.Logging;
namespace PCL;
internal sealed class CrashModInfo
{
public string? ModId { get; init; }
public string? DisplayName { get; init; }
public string? FileName { get; init; }
public string? Version { get; init; }
public string Source { get; init; } = "";
public string DisplayNameOrFileName =>
!string.IsNullOrWhiteSpace(DisplayName) ? DisplayName :
!string.IsNullOrWhiteSpace(FileName) ? FileName :
ModId ?? "";
}
internal sealed class CrashModIndex
{
private readonly List<CrashModInfo> _mods = [];
public static CrashModIndex Create(CrashLogSet logs, McInstance? instance)
{
var index = new CrashModIndex();
index._ReadCrashReport(logs.CrashReport?.Text);
index._ReadForgeDebugLog(logs.Debug?.Text);
index._ReadLoaderMessages(logs.Game?.Text);
if (instance is not null)
index._ReadInstanceModFiles(instance);
index._NormalizeAndDeduplicate();
return index;
}
public IEnumerable<CrashModInfo> ResolveMany(IEnumerable<string> hints)
{
var found = new List<CrashModInfo>();
foreach (var hint in hints)
foreach (var mod in Resolve(hint))
if (!found.Any(item => _SameDisplay(item, mod)))
found.Add(mod);
return found;
}
public IEnumerable<CrashModInfo> Resolve(string? hint)
{
if (string.IsNullOrWhiteSpace(hint))
return [];
var normalizedHint = _Normalize(hint);
if (string.IsNullOrWhiteSpace(normalizedHint))
return [];
var exact = _mods
.Where(mod =>
string.Equals(_Normalize(mod.ModId), normalizedHint, StringComparison.Ordinal) ||
string.Equals(_Normalize(mod.FileName), normalizedHint, StringComparison.Ordinal) ||
string.Equals(_Normalize(mod.DisplayName), normalizedHint, StringComparison.Ordinal))
.ToList();
if (exact.Count != 0)
return exact;
return _mods
.Where(mod =>
_Normalize(mod.ModId).Contains(normalizedHint, StringComparison.Ordinal) ||
_Normalize(mod.FileName).Contains(normalizedHint, StringComparison.Ordinal) ||
_Normalize(mod.DisplayName).Contains(normalizedHint, StringComparison.Ordinal))
.ToList();
}
public List<string> ResolveToDisplayNames(IEnumerable<string> hints)
{
var hintList = hints
.Where(item => !string.IsNullOrWhiteSpace(item))
.Select(item => item.Trim())
.Distinct()
.ToList();
var mods = ResolveMany(hintList)
.Select(mod => mod.DisplayNameOrFileName)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.Ordinal)
.ToList();
return mods.Count != 0 ? mods : hintList;
}
private void _ReadCrashReport(string? text)
{
if (string.IsNullOrEmpty(text) ||
!text.Contains("A detailed walkthrough of the error", StringComparison.Ordinal))
return;
var details = text.Replace("A detailed walkthrough of the error", "¨");
var isFabric = details.Contains("Fabric Mods", StringComparison.Ordinal);
if (isFabric)
{
details = details.Replace("Fabric Mods", "¨");
LogWrapper.Info("Crash", "崩溃报告中检测到 Fabric Mod 信息格式");
}
var isQuilt = details.Contains("quilt-loader", StringComparison.Ordinal);
if (isQuilt)
{
details = details.Replace("Mod Table Version", "¨");
LogWrapper.Info("Crash", "崩溃报告中检测到 Quilt Mod 信息格式");
}
details = CrashText.AfterLast(details, "¨");
foreach (var rawLine in details.Split('\n'))
{
var line = rawLine.Trim('\r', '\n');
if (string.IsNullOrWhiteSpace(line))
continue;
if (isFabric && line.StartsWith(
"\t\t",
StringComparison.Ordinal) && !CrashRegex.IsMatch(line, @"\t\tfabric[\w-]*: Fabric"))
{
_Add(new CrashModInfo
{
ModId = CrashRegex.First(line, @"(?<=\t\t)[^:]+"),
DisplayName = CrashRegex.First(line, @"(?<=: )[^\n]+(?= [^\n]+)"),
Version = CrashRegex.First(line, @"(?<= )[\w\.-]+$"),
Source = "crash-report-fabric"
});
continue;
}
if (line.Contains(".jar", StringComparison.OrdinalIgnoreCase) &&
line.Length - line.Replace(".jar", "", StringComparison.OrdinalIgnoreCase).Length == 4)
_Add(new CrashModInfo
{
FileName = CrashRegex.First(line,
@"(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar",
RegexOptions.IgnoreCase),
DisplayName = CrashRegex.First(line, @"(?<=\t)[^\t\|]+(?=\s+\()"),
ModId = CrashRegex.First(line, @"(?<=\| )[\w\.-]+(?= \|)"),
Source = "crash-report-forge"
});
}
}
private void _ReadForgeDebugLog(string? text)
{
if (string.IsNullOrEmpty(text))
return;
foreach (var line in CrashRegex.All(text, "(?<=valid mod file ).*", RegexOptions.Multiline))
_Add(new CrashModInfo
{
FileName = CrashRegex.First(line, ".*(?= with)"),
ModId = CrashRegex.First(line, @"(?<=with \{)[^\}]+"),
Version = CrashRegex.First(line, @"(?<=versions \{)[^\}]+"),
Source = "debug-log"
});
}
private void _ReadLoaderMessages(string? text)
{
if (string.IsNullOrEmpty(text))
return;
foreach (var line in CrashRegex.All(text, @"(?<=ModID: )[^,\n]+|(?<=ModId )[^\n]+(?= for )",
RegexOptions.IgnoreCase))
_Add(new CrashModInfo
{
ModId = line.Trim(),
Source = "loader-message"
});
}
private void _ReadInstanceModFiles(McInstance instance)
{
foreach (var directory in _GetCandidateModDirectories(instance))
try
{
var info = new DirectoryInfo(directory);
if (!info.Exists)
continue;
foreach (var file in info.EnumerateFiles("*.jar"))
_Add(new CrashModInfo
{
FileName = file.Name,
DisplayName = Path.GetFileNameWithoutExtension(file.Name),
Source = "instance-mods"
});
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "读取实例 Mod 文件列表失败(" + directory + "");
}
}
private static IEnumerable<string> _GetCandidateModDirectories(McInstance instance)
{
yield return Path.Combine(instance.PathInstance, "mods");
string? pathIndie = null;
try
{
pathIndie = instance.PathIndie;
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "读取实例隔离路径失败");
}
if (!string.IsNullOrEmpty(pathIndie))
yield return Path.Combine(pathIndie, "mods");
}
private void _Add(CrashModInfo mod)
{
if (string.IsNullOrWhiteSpace(mod.ModId) &&
string.IsNullOrWhiteSpace(mod.DisplayName) &&
string.IsNullOrWhiteSpace(mod.FileName))
return;
_mods.Add(mod);
}
private void _NormalizeAndDeduplicate()
{
var distinct = new List<CrashModInfo>();
foreach (var mod in _mods
.Where(mod => !distinct.Any(item => _SameDisplay(item, mod))))
distinct.Add(mod);
_mods.Clear();
_mods.AddRange(distinct);
LogWrapper.Info("Crash", "构建 Mod 索引,找到 " + _mods.Count + " 个候选 Mod");
}
private static bool _SameDisplay(CrashModInfo a, CrashModInfo b)
{
return string.Equals(_Normalize(a.ModId), _Normalize(b.ModId), StringComparison.Ordinal) &&
string.Equals(_Normalize(a.FileName), _Normalize(b.FileName), StringComparison.Ordinal) &&
string.Equals(_Normalize(a.DisplayName), _Normalize(b.DisplayName), StringComparison.Ordinal);
}
private static string _Normalize(string? value)
{
return (value ?? string.Empty)
.ToLowerInvariant()
.Replace("_", "")
.Replace("-", "")
.Replace(" ", "")
.Replace(".jar", "");
}
}
@@ -0,0 +1,21 @@
namespace PCL;
internal sealed class CrashRule
{
public required CrashCause Cause { get; init; }
public required DetectionPhase Phase { get; init; }
public CrashConfidence Confidence { get; init; } = CrashConfidence.High;
public bool StopOnMatch { get; init; }
public required Func<CrashRuleInput, CrashFinding?> Evaluate { get; init; }
}
internal sealed class CrashRuleInput
{
public required CrashLogSet Logs { get; init; }
public required CrashModIndex ModIndex { get; init; }
}
@@ -0,0 +1,268 @@
namespace PCL;
internal static class CrashRuleCatalog
{
public static IReadOnlyList<CrashRule> Rules { get; } =
[
_Contains(
DetectionPhase.Fatal,
CrashLogKind.CrashReport,
"Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass",
CrashCause.JavaTooNew),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"Found multiple arguments for option fml.forgeVersion, but you asked for only one",
CrashCause.MultipleForgeInInstanceJson),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"The driver does not appear to support OpenGL",
CrashCause.UnsupportedOpenGl),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"java.lang.ClassCastException: java.base/jdk",
CrashCause.UsingJdk),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"java.lang.ClassCastException: class jdk.",
CrashCause.UsingJdk),
_ContainsAny(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"TRANSFORMER/net.optifine/net.optifine.reflect.Reflector.<clinit>(Reflector.java",
"java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.texture.SpriteContents.<init>",
"java.lang.NoSuchMethodError: 'java.lang.String com.mojang.blaze3d.systems.RenderSystem.getBackendDescription",
"java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.block.model.BakedQuad.<init>",
"java.lang.NoSuchMethodError: 'void net.minecraftforge.client.gui.overlay.ForgeGui.renderSelectedItemName",
"java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager",
"java.lang.NoSuchMethodError: 'net.minecraft.network.chat.FormattedText net.minecraft.client.gui.Font.ellipsize"
],
CrashCause.OptiFineForgeIncompatible),
_ContainsAny(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"Open J9 is not supported",
"OpenJ9 is incompatible",
".J9VMInternals."
],
CrashCause.UsingOpenJ9),
_ContainsAny(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"java.lang.NoSuchFieldException: ucp",
"because module java.base does not export",
"java.lang.ClassNotFoundException: jdk.nashorn.api.scripting.NashornScriptEngineFactory",
"java.lang.ClassNotFoundException: java.lang.invoke.LambdaMetafactory"
],
CrashCause.JavaTooNew),
_ContainsAny(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"The directories below appear to be extracted jar files. Fix this before you continue.",
"Extracted mod jars found, loading will NOT continue"
],
CrashCause.ExtractedModFile),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker",
CrashCause.MissingMixinBootstrap),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"Couldn't set pixel format",
CrashCause.PixelFormatNotSupported),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.",
CrashCause.ShadersModWithOptiFine),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier",
CrashCause.OldForgeNewJavaIncompatible),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"1282: Invalid operation",
CrashCause.OpenGl1282),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"Maybe try a lower resolution resourcepack?",
CrashCause.ResourcePackTooLarge),
_ContainsAll(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"java.lang.NoSuchMethodError: net.minecraft.world.server.ChunkManager$ProxyTicketManager.shouldForceTicks(J)Z",
"OptiFine"
],
CrashCause.OptiFineWorldLoadCrash),
_ContainsAny(
DetectionPhase.Fatal,
CrashLogKind.Game,
["Unsupported class file major version", "Unsupported major.minor version"], CrashCause.JavaIncompatible),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"com.electronwill.nightconfig.core.io.ParsingException: Not enough data available",
CrashCause.NightConfigBug),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"Cannot find launch target fmlclient, unable to launch",
CrashCause.IncompleteForgeInstallation),
_ContainsAll(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"Invalid paths argument, contained no existing paths",
@"libraries\net\minecraftforge\fmlcore"
],
CrashCause.IncompleteForgeInstallation),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"Invalid module name: '' is not a Java identifier",
CrashCause.InvalidModFileName),
_ContainsAny(
DetectionPhase.Fatal,
CrashLogKind.Game,
[
"has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to",
"java.lang.RuntimeException: java.lang.NoSuchMethodException: no such method: sun.misc.Unsafe.defineAnonymousClass(Class,byte[],Object[])Class/invokeVirtual",
"java.lang.IllegalArgumentException: The requested compatibility level JAVA_11 could not be set. Level is not supported by the active JRE or ASM version"
],
CrashCause.ModRequiresJava11),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.Game,
"Invalid maximum heap size",
CrashCause.X86JavaMemoryLimit),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.CrashReport,
"maximum id range exceeded",
CrashCause.TooManyModsIdLimit),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.CrashReport,
"Pixel format not accelerated",
CrashCause.PixelFormatNotSupported),
_Contains(
DetectionPhase.Fatal,
CrashLogKind.CrashReport,
"Manually triggered debug crash",
CrashCause.ManualDebugCrash)
];
private static CrashRule _Contains(
DetectionPhase phase,
CrashLogKind source,
string pattern,
CrashCause cause,
bool stopOnMatch = true)
{
return new CrashRule
{
Phase = phase,
Cause = cause,
StopOnMatch = stopOnMatch,
Evaluate = input =>
{
var text = input.Logs.GetText(source);
return text?.Contains(pattern, StringComparison.Ordinal) != true
? null
: _CreateFinding(cause, CrashConfidence.High, source, pattern, stopOnMatch);
}
};
}
private static CrashRule _ContainsAny(
DetectionPhase phase,
CrashLogKind source,
IReadOnlyList<string> patterns,
CrashCause cause,
bool stopOnMatch = true)
{
return new CrashRule
{
Phase = phase,
Cause = cause,
StopOnMatch = stopOnMatch,
Evaluate = input =>
{
var text = input.Logs.GetText(source);
if (string.IsNullOrEmpty(text))
return null;
var pattern = patterns.FirstOrDefault(item => text.Contains(item, StringComparison.Ordinal));
return pattern is null
? null
: _CreateFinding(cause, CrashConfidence.High, source, pattern, stopOnMatch);
}
};
}
private static CrashRule _ContainsAll(
DetectionPhase phase,
CrashLogKind source,
IReadOnlyList<string> patterns,
CrashCause cause,
bool stopOnMatch = true)
{
return new CrashRule
{
Phase = phase,
Cause = cause,
StopOnMatch = stopOnMatch,
Evaluate = input =>
{
var text = input.Logs.GetText(source);
if (string.IsNullOrEmpty(text) ||
patterns.Any(pattern => !text.Contains(pattern, StringComparison.Ordinal)))
return null;
return _CreateFinding(
cause,
CrashConfidence.High,
source,
string.Join(" | ", patterns),
stopOnMatch);
}
};
}
private static CrashFinding _CreateFinding(
CrashCause cause,
CrashConfidence confidence,
CrashLogKind source,
string pattern,
bool stopOnMatch)
{
return new CrashFinding(
cause,
confidence,
[
new CrashEvidence
{
Source = source,
Pattern = pattern,
Value = pattern,
DisplayKind = "pattern"
}
])
{
ShouldStop = stopOnMatch
};
}
}
@@ -0,0 +1,199 @@
using System.Text.RegularExpressions;
using PCL.Core.Logging;
namespace PCL;
internal sealed partial class CrashStackAnalyzer
{
private static readonly HashSet<string> IgnoredPrefixes = new(StringComparer.Ordinal)
{
"java", "sun", "javax", "jdk", "com.sun",
"com.mojang", "net.minecraft", "MojangTricksIntelDriversForPerformance_javaw",
"net.minecraftforge", "cpw.mods", "net.fabricmc", "org.quiltmc", "org.spongepowered", "com.mumfrey",
"org.lwjgl", "paulscode.sound", "com.google", "org.apache", "com.electronwill.nightconfig", "it.unimi.dsi",
"oolloo"
};
private static readonly HashSet<string> IgnoredWords = new(StringComparer.OrdinalIgnoreCase)
{
"com", "org", "net", "top", "dev", "gitlab", "github",
"sun", "lib", "nio", "api", "asm", "reflect", "internal", "microsoft",
"mcp", "fml", "forge", "fabricmc", "neoforge", "neoforged", "minecraftforge", "minecraft", "mojang",
"mod", "mods", "modapi", "jar", "loader", "launch", "preinit", "preload", "init", "setup", "plugin",
"mixin", "mixins", "injection", "transformer", "transformers", "spongepowered",
"game", "world", "server", "client", "common", "entity", "block", "item", "tile", "blockentity",
"gui", "model", "render", "shader", "optifine",
"config", "data", "file", "read", "recipe", "content", "general",
"event", "events", "handler", "listeners", "assist", "override",
"netty", "packet", "channel",
"task", "pool", "scheduler", "systems", "system", "modules", "service", "platform",
"core", "main", "base", "util", "impl", "done", "map", "load", "machine",
"dsi", "unimi", "fastutil", "lwjgl", "oshi", "electronwill",
"compat", "universal", "multipart"
};
public CrashFinding? Analyze(
CrashLogSet logs,
CrashModIndex modIndex)
{
if (!_ShouldAnalyzeStack(logs.All))
{
LogWrapper.Info("Crash", "可能并未安装 Mod,不进行堆栈分析");
return null;
}
var stackText = _CollectStackText(logs);
var packages = _ExtractPackages(stackText).ToList();
var keywords = _ExtractKeywords(packages).ToList();
if (keywords.Count is 0 or > 10)
{
if (keywords.Count > 10)
LogWrapper.Info("Crash", "关键词过多,考虑匹配出错,不纳入考虑");
return null;
}
var mods = modIndex.ResolveMany(keywords).ToList();
if (mods.Count != 0)
return new CrashFinding(
CrashCause.StackModNameFound,
CrashConfidence.Medium,
mods.Select(mod => new CrashEvidence
{
Source = CrashLogKind.All,
Value = mod.DisplayNameOrFileName,
DisplayKind = "mod"
}))
{
ShouldStop = true
};
return new CrashFinding(
CrashCause.StackKeywordFound,
CrashConfidence.Low,
keywords.Select(keyword => new CrashEvidence
{
Source = CrashLogKind.All,
Value = keyword,
DisplayKind = "keyword"
}))
{
ShouldStop = true
};
}
private static bool _ShouldAnalyzeStack(string allLogs)
{
return allLogs.Contains("forge", StringComparison.OrdinalIgnoreCase) ||
allLogs.Contains("fabric", StringComparison.OrdinalIgnoreCase) ||
allLogs.Contains("quilt", StringComparison.OrdinalIgnoreCase) ||
allLogs.Contains("liteloader", StringComparison.OrdinalIgnoreCase);
}
private static string _CollectStackText(CrashLogSet logs)
{
var result = new List<string>();
if (logs.CrashReport?.Text is not null)
{
LogWrapper.Info("Crash", "开始进行崩溃日志堆栈分析");
result.Add(CrashText.BeforeFirst(logs.CrashReport.Text, "System Details"));
}
if (logs.Game?.Text is not null)
{
var fatals = CrashRegex.All(logs.Game.Text, @"/FATAL] .+?(?=[\n]+\[)");
if (logs.Game.Text.Contains("Unreported exception thrown!", StringComparison.Ordinal))
fatals.Add(
CrashText.Between(logs.Game.Text,
"Unreported exception thrown!",
"at oolloo.jlw.Wrapper"));
LogWrapper.Info("Crash", "开始进行 Minecraft 日志堆栈分析,发现 " + fatals.Count + " 个报错项");
result.AddRange(fatals);
}
if (logs.HsErr?.Text is not null)
{
LogWrapper.Info("Crash", "开始进行虚拟机堆栈分析");
result.Add(CrashText.Between(logs.HsErr.Text, "T H R E A D", "Registers:"));
}
return "\n" + string.Join("\n", result) + "\n";
}
private static IEnumerable<string> _ExtractPackages(string stackText)
{
var packages = new List<string>();
packages.AddRange(
PackageRegex()
.Matches(stackText)
.Select(match => match.Value));
packages.AddRange(
MixinStackRegex()
.Matches(stackText)
.Select(match => match.Value.Replace("$", ".")));
var possibleStacks = packages
.Select(item => item.Trim())
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.Ordinal)
.Where(item => !IgnoredPrefixes.Any(prefix => item.StartsWith(prefix, StringComparison.Ordinal)))
.ToList();
LogWrapper.Info("Crash", "找到 " + possibleStacks.Count + " 条可能的堆栈信息");
foreach (var stack in possibleStacks)
LogWrapper.Info("Crash", " - " + stack);
return possibleStacks;
}
private static IEnumerable<string> _ExtractKeywords(IEnumerable<string> packages)
{
var words = new List<string>();
foreach (var package in packages)
{
var split = package.Split('.');
for (var i = 0; i <= Math.Min(3, split.Length - 1); i++)
{
var word = split[i].Trim();
if (word.Length <= 2 ||
word.StartsWith("func_", StringComparison.Ordinal) ||
IgnoredWords.Contains(word))
continue;
words.Add(word);
}
}
var result = words
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
LogWrapper.Info("Crash", "从堆栈信息中找到 " + result.Count + " 个可能的 Mod ID 关键词");
if (result.Count != 0)
LogWrapper.Info("Crash", " - " + string.Join(", ", result));
return result;
}
[GeneratedRegex(
@"(?<=\n[^{]+)[a-zA-Z_]+\w+\.[a-zA-Z_]+[\w\.]+(?=\.[\w\.$]+\.)",
RegexOptions.Compiled)]
private static partial Regex PackageRegex();
[GeneratedRegex(
@"(?<=at [^(]+?\.\w+\$\w+\$)[\w\$]+?(?=\$\w+\()",
RegexOptions.Compiled)]
private static partial Regex MixinStackRegex();
}
@@ -0,0 +1,8 @@
namespace PCL;
internal enum DetectionPhase
{
Fatal,
Primary,
Secondary
}
@@ -0,0 +1,405 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.Utils.Codecs;
using PCL.Core.Utils.OS;
using PCL.Core.Utils.Secret;
namespace PCL;
internal sealed class CrashReportExporter
{
private const string ReportFolderName = "Report";
private const string LaunchScriptFileName = "启动脚本.bat";
private const string RawOutputFileName = "游戏崩溃前的输出.txt";
private const string LauncherLogFileName = "PCL CE 启动器日志.txt";
private const string EnvironmentFileName = "环境与启动信息.txt";
private const string ModInfoFileName = "模组列表.txt";
public void Export(
CrashAnalysisContext context,
string targetZipPath,
IEnumerable<string>? extraFiles)
{
var targetFolder = Basics.GetParentPathOrEmpty(targetZipPath);
Directory.CreateDirectory(targetFolder);
if (File.Exists(targetZipPath))
File.Delete(targetZipPath);
ModBase.FeedbackInfo();
var reportFolder = Path.Combine(context.TempFolder, ReportFolderName);
if (Directory.Exists(reportFolder))
CrashFileIo.DeleteDirectory(reportFolder);
Directory.CreateDirectory(reportFolder);
try
{
foreach (var outputFile in _CollectOutputFiles(context, extraFiles))
_CopyFileToReport(reportFolder, outputFile);
_WriteEnvironmentInfo(reportFolder);
_WriteModInfo(reportFolder, context.Instance);
ZipFile.CreateFromDirectory(reportFolder, targetZipPath);
}
finally
{
CrashFileIo.DeleteDirectory(reportFolder);
}
}
private static void _CopyFileToReport(
string reportFolder,
string outputFile)
{
if (!File.Exists(outputFile))
return;
var fileName = _GetExportFileName(outputFile, out var fileEncoding);
fileEncoding ??= EncodingDetector.DetectEncoding(CrashFileIo.ReadBytes(outputFile));
var fileContent = CrashFileIo.ReadText(outputFile, fileEncoding);
fileContent = _SanitizeFileContent(fileContent, fileName);
CrashFileIo.WriteText(
Path.Combine(reportFolder, fileName),
fileContent,
fileEncoding);
LogWrapper.Info("Crash", $"导出文件:{fileName},编码:{fileEncoding.HeaderName}");
}
private static string _GetExportFileName(
string outputFile,
out Encoding? fileEncoding)
{
fileEncoding = null;
var fileName = Path.GetFileName(outputFile);
switch (fileName)
{
case "LatestLaunch.bat":
return LaunchScriptFileName;
case "RawOutput.log":
fileEncoding = Encoding.UTF8;
return RawOutputFileName;
}
var currentLogFile = LogWrapper.CurrentLogger.CurrentLogFiles.LastOrDefault();
var currentLogFileName = currentLogFile is null ? null : CrashText.AfterLast(currentLogFile, @"\");
if (currentLogFileName != fileName) return fileName;
fileEncoding = Encoding.UTF8;
return LauncherLogFileName;
}
private static IReadOnlyList<string> _CollectOutputFiles(
CrashAnalysisContext context,
IEnumerable<string>? extraFiles)
{
if (extraFiles is not null)
context.OutputFiles.AddRange(extraFiles);
return context.OutputFiles;
}
private static string _SanitizeFileContent(
string fileContent,
string fileName)
{
var tokenMask = fileName == LaunchScriptFileName ? 'F' : '*';
fileContent = McLogFilter.FilterAccessToken(fileContent, tokenMask);
return McLogFilter.FilterUserName(fileContent, '*');
}
private static void _WriteEnvironmentInfo(string reportFolder)
{
var launcherLog = CrashText.BeforeFirst(
CrashText.AfterLast(_ReadReportFile(reportFolder, LauncherLogFileName), "[Launch] ~ 基础参数 ~"),
"开始 Minecraft 日志监控");
var launchScript = _ReadReportFile(reportFolder, LaunchScriptFileName);
var envInfo = new StringBuilder();
_AppendLauncherInfo(envInfo);
_AppendProfileInfo(envInfo, launcherLog);
_AppendInstanceInfo(envInfo, launcherLog, launchScript);
_AppendEnvironmentInfo(envInfo, launcherLog);
CrashFileIo.WriteText(
Path.Combine(reportFolder, EnvironmentFileName),
envInfo.ToString(),
Encoding.UTF8);
}
private static void _AppendLauncherInfo(StringBuilder builder)
{
builder.AppendLine(Lang.Text("Crash.Report.Environment.LauncherVersion", Basics.VersionName));
builder.AppendLine(Lang.Text("Crash.Report.Environment.LauncherId", Identify.LauncherId));
builder.AppendLine();
}
private static void _AppendProfileInfo(
StringBuilder builder,
string launcherLog)
{
builder.AppendLine(Lang.Text("Crash.Report.Environment.ProfileSection"));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.ProfileName",
_ExtractLauncherValue(launcherLog, "玩家用户名:"),
_ExtractLauncherValue(launcherLog, "验证方式:")));
builder.AppendLine();
}
private static void _AppendInstanceInfo(
StringBuilder builder,
string launcherLog,
string launchScript)
{
builder.AppendLine(Lang.Text("Crash.Report.Environment.InstanceSection"));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.SelectedJava",
_ExtractLauncherValue(launcherLog, "Java 信息:")));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.Log4j2NoLookups",
!launchScript.Contains("-Dlog4j2.formatMsgNoLookups=false", StringComparison.OrdinalIgnoreCase)));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.MinecraftFolder",
_ExtractLauncherValue(launcherLog, "MC 文件夹:")));
builder.AppendLine();
}
private static void _AppendEnvironmentInfo(
StringBuilder builder,
string launcherLog)
{
builder.AppendLine(Lang.Text("Crash.Report.Environment.EnvironmentSection"));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.OperatingSystem",
SystemInfo.OSInfo,
!SystemInfo.Is32BitSystem,
SystemInfo.IsArm64System));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.Cpu",
HardwareInfo.CPUName));
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.MemoryAllocation",
_ExtractLauncherValue(launcherLog, "分配的内存:"),
Lang.Number(HardwareInfo.SystemMemorySize / 1024d, "N2"),
Lang.Number(HardwareInfo.SystemMemorySize, "N0")));
for (var i = 0; i < HardwareInfo.GPUs.Count; i++)
{
var gpu = HardwareInfo.GPUs[i];
builder.AppendLine(
Lang.Text(
"Crash.Report.Environment.Gpu",
i,
gpu.Name,
_FormatGpuMemory(gpu.Memory),
gpu.DriverVersion));
}
}
private static string _ExtractLauncherValue(
string launcherLog,
string key)
{
return CrashText.Between(launcherLog, key, "[")
.TrimEnd('[')
.Trim();
}
private static string _FormatGpuMemory(long memory)
{
return memory >= 4095L
? ">= " + memory
: memory.ToString();
}
private static string _ReadReportFile(
string reportFolder,
string fileName)
{
var filePath = Path.Combine(reportFolder, fileName);
return File.Exists(filePath)
? CrashFileIo.ReadText(filePath)
: "";
}
private static void _WriteModInfo(string reportFolder, McInstance? instance)
{
if (instance is null)
return;
try
{
var modsFolderName = ModLocalComp.GetPathNameByCompType(ModComp.CompType.Mod);
var modsFolder = instance.Info.HasLabyMod
? Path.Combine(instance.PathIndie, "labymod-neo", "fabric", instance.Info.VanillaName, modsFolderName)
: Path.Combine(instance.PathIndie, modsFolderName);
if (!Directory.Exists(modsFolder))
return;
// 老 ForgeDrop < 130)的启用 Mod 位于 mods/<版本名> 子目录,需一并扫描
var scanFolders = new List<string> { modsFolder };
if (instance.Info.HasForge && instance.Info.Drop < 130)
{
var versionSubFolder = Path.Combine(modsFolder, instance.Info.VanillaName);
if (Directory.Exists(versionSubFolder))
scanFolders.Add(versionSubFolder);
}
var activeMods = new List<ModLocalComp.LocalCompFile>();
foreach (var folder in scanFolders)
foreach (var file in Directory.GetFiles(folder))
{
if (!ModLocalComp.LocalCompFile.IsModFile(file)
|| file.EndsWith(".disabled", StringComparison.OrdinalIgnoreCase)
|| file.EndsWith(".old", StringComparison.OrdinalIgnoreCase))
continue;
var mod = new ModLocalComp.LocalCompFile(file);
mod.Load();
if (mod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
activeMods.Add(mod);
}
activeMods = activeMods
.OrderBy(m => m.Name ?? m.FileName, StringComparer.OrdinalIgnoreCase)
.ToList();
var sb = new StringBuilder();
var modsByModId = new Dictionary<string, List<ModLocalComp.LocalCompFile>>(StringComparer.OrdinalIgnoreCase);
foreach (var mod in activeMods)
{
if (string.IsNullOrEmpty(mod.ModId))
continue;
if (!modsByModId.TryGetValue(mod.ModId, out var list))
modsByModId[mod.ModId] = list = new List<ModLocalComp.LocalCompFile>();
list.Add(mod);
}
var duplicates = new List<string>();
foreach (var host in activeMods)
foreach (var embedded in _FlattenEmbedded(host.EmbeddedMods))
{
if (string.IsNullOrEmpty(embedded.ModId)
|| !modsByModId.TryGetValue(embedded.ModId, out var matches))
continue;
foreach (var other in matches)
{
if (ReferenceEquals(other, host))
continue;
duplicates.Add(Lang.Text("Crash.Report.JarInJarMod.DuplicateEntry", other.Name, other.FileName, host.Name, embedded.Name ?? embedded.ModId));
}
}
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateSection"));
if (duplicates.Count == 0)
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateNone"));
else
{
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateDescription"));
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateHeuristic"));
foreach (var line in duplicates.Distinct())
sb.AppendLine("\t|-> " + line);
}
sb.AppendLine().AppendLine("----------------------------").AppendLine();
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.ModListSection"));
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.ModListDirectory", modsFolder));
sb.AppendLine("|-> mods");
foreach (var mod in activeMods)
{
var line = "| |-> " + (mod.Name ?? mod.FileName);
if (!string.IsNullOrWhiteSpace(mod.Version))
line += $" ({mod.Version})";
if (mod.Name != mod.FileName)
line += $" [{mod.FileName}]";
sb.AppendLine(line);
}
sb.AppendLine().AppendLine("----------------------------").AppendLine();
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.JarInJarSection"));
var hasJij = false;
foreach (var mod in activeMods)
{
if (!mod.EmbeddedMods.Any())
continue;
hasJij = true;
sb.AppendLine(mod.Name ?? mod.FileName);
_AppendEmbeddedMods(sb, mod.EmbeddedMods, 1);
sb.AppendLine();
}
if (!hasJij)
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.JarInJarNone"));
CrashFileIo.WriteText(Path.Combine(reportFolder, ModInfoFileName), sb.ToString(), Encoding.UTF8);
LogWrapper.Info("Crash", "已导出模组列表及 Jar-in-Jar 信息");
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "导出模组信息失败");
}
}
private static IEnumerable<ModLocalComp.LocalCompFile> _FlattenEmbedded(List<ModLocalComp.LocalCompFile> mods)
{
foreach (var mod in mods)
{
yield return mod;
if (mod.EmbeddedMods.Any())
foreach (var child in _FlattenEmbedded(mod.EmbeddedMods))
yield return child;
}
}
private static void _AppendEmbeddedMods(StringBuilder builder, List<ModLocalComp.LocalCompFile> mods, int depth)
{
var indent = new string('\t', depth);
foreach (var mod in mods)
{
var line = indent + "|-> " + (mod.Name ?? mod.ModId ?? "?");
if (!string.IsNullOrWhiteSpace(mod.Version))
line += $" ({mod.Version})";
builder.AppendLine(line);
if (mod.EmbeddedMods.Any())
_AppendEmbeddedMods(builder, mod.EmbeddedMods, depth + 1);
}
}
}
@@ -0,0 +1,8 @@
namespace PCL;
internal enum CrashConfidence
{
High,
Medium,
Low
}
@@ -0,0 +1,127 @@
using System.IO;
using PCL.Core.App;
using PCL.Core.Logging;
namespace PCL;
internal sealed class CrashLogCollector(CrashAnalysisContext context)
{
public void Collect(string versionPathIndie, IList<string>? latestLog)
{
LogWrapper.Info("Crash", "步骤 1:收集日志文件");
var possibleLogs = _FindPossibleLogFiles(versionPathIndie)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var recentLogs = possibleLogs.Where(IsRecentNonEmptyFile).ToList();
if (recentLogs.Count == 0)
LogWrapper.Info("Crash", "未发现可能可用的日志文件");
foreach (var filePath in recentLogs)
TryAddLogFile(filePath, "读取可能的崩溃日志文件失败");
AddCapturedOutput(latestLog);
LogWrapper.Info("Crash", "步骤 1:收集日志文件完成,收集到 " + context.RawFiles.Count + " 个文件");
}
private static IEnumerable<string> _FindPossibleLogFiles(string versionPathIndie)
{
var possibleLogs = new List<string>();
try
{
var dirInfo = new DirectoryInfo(Path.Combine(versionPathIndie, "crash-reports"));
if (dirInfo.Exists)
possibleLogs.AddRange(dirInfo.EnumerateFiles().Select(file => file.FullName));
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "收集 Minecraft 崩溃日志文件夹下的日志失败");
}
try
{
var rootDirectory = new DirectoryInfo(versionPathIndie).Parent?.Parent;
if (rootDirectory is not null && rootDirectory.Exists)
possibleLogs.AddRange(
rootDirectory.EnumerateFiles()
.Where(file => file.Extension == ".log")
.Select(file => file.FullName));
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "收集 Minecraft 主文件夹下的日志失败");
}
try
{
var instanceDirectory = new DirectoryInfo(versionPathIndie);
if (instanceDirectory.Exists)
possibleLogs.AddRange(
instanceDirectory.EnumerateFiles()
.Where(file => file.Extension == ".log")
.Select(file => file.FullName));
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "收集 Minecraft 隔离文件夹下的日志失败");
}
possibleLogs.Add(Path.Combine(versionPathIndie, "logs", "latest.log"));
var launchScript = CrashFileIo.ReadText(Path.Combine(Basics.ExecutableDirectory, "PCL", "LatestLaunch.bat"));
if (launchScript.Contains("-Dlog4j2.formatMsgNoLookups=false", StringComparison.OrdinalIgnoreCase))
possibleLogs.Add(Path.Combine(versionPathIndie, "logs", "debug.log"));
return possibleLogs;
}
private static bool IsRecentNonEmptyFile(string filePath)
{
try
{
var info = new FileInfo(filePath);
if (!info.Exists || info.Length <= 0L)
return false;
var ageMinutes = Math.Abs((info.LastWriteTime - DateTime.Now).TotalMinutes);
if (ageMinutes >= 3d)
return false;
LogWrapper.Info("Crash", "可能可用的日志文件:" + filePath + "" + Math.Round(ageMinutes, 1) + " 分钟)");
return true;
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "确认崩溃日志时间失败(" + filePath + "");
return false;
}
}
private void TryAddLogFile(string filePath, string errorMessage)
{
try
{
context.RawFiles.Add(
new CrashLogEntry(filePath, CrashFileIo.ReadText(filePath).Split("\r\n".ToCharArray())));
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", errorMessage + "" + filePath + "");
}
}
private void AddCapturedOutput(IList<string>? latestLog)
{
if (latestLog is null || !latestLog.Any())
return;
var rawOutput = string.Join("\r\n", latestLog);
LogWrapper.Info("Crash", "以下为游戏输出的最后一段内容:" + "\r\n" + rawOutput);
var rawOutputPath = Path.Combine(context.TempFolder, "RawOutput.log");
CrashFileIo.WriteText(rawOutputPath, rawOutput);
context.RawFiles.Add(new CrashLogEntry(rawOutputPath, latestLog.ToArray()));
latestLog.Clear();
}
}
@@ -0,0 +1,14 @@
using System.IO;
namespace PCL;
internal sealed class CrashLogEntry(
string fullPath,
IReadOnlyList<string> lines)
{
public string FullPath { get; } = fullPath;
public IReadOnlyList<string> Lines { get; } = lines;
public string FileName => Path.GetFileName(FullPath);
}
@@ -0,0 +1,66 @@
using System.IO;
using PCL.Core.Logging;
namespace PCL;
internal sealed class CrashLogImporter(CrashAnalysisContext context)
{
public void Import(string filePath)
{
LogWrapper.Info("Crash", "步骤 1:自主导入日志文件");
if (!_TryExtractArchive(filePath))
{
CrashFileIo.CopyFile(filePath, Path.Combine(
context.TempFolder,
"Temp",
Path.GetFileName(filePath)));
LogWrapper.Info("Crash", "已复制导入的日志文件:" + filePath);
}
foreach (
var targetFile in new DirectoryInfo(Path.Combine(context.TempFolder, "Temp"))
.EnumerateFiles("*", SearchOption.AllDirectories)
.ToList())
try
{
if (!targetFile.Exists || targetFile.Length == 0L)
continue;
var ext = targetFile.Extension.ToLowerInvariant();
if (ext is ".log" or ".txt")
context.RawFiles.Add(new CrashLogEntry(targetFile.FullName,
CrashFileIo.ReadText(targetFile.FullName).Split("\r\n".ToCharArray())));
else
File.Delete(targetFile.FullName);
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "导入单个日志文件失败");
}
LogWrapper.Info("Crash", "步骤 1:自主导入日志文件,收集到 " + context.RawFiles.Count + " 个文件");
}
private bool _TryExtractArchive(string filePath)
{
try
{
var info = new FileInfo(filePath);
if (!info.Exists || info.Length <= 0L)
return false;
if (!CrashFileIo.CanExtractArchive(filePath))
return false;
CrashFileIo.ExtractFile(filePath, Path.Combine(context.TempFolder, "Temp"));
LogWrapper.Info("Crash", "已解压导入的日志文件:" + filePath);
return true;
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "尝试解压导入文件失败,将按普通文件处理(" + filePath + "");
return false;
}
}
}
@@ -0,0 +1,10 @@
namespace PCL;
internal enum CrashLogKind
{
Game,
Debug,
CrashReport,
HsErr,
All
}
@@ -0,0 +1,416 @@
using System.IO;
using System.Text;
using PCL.Core.Logging;
namespace PCL;
internal sealed class CrashLogPreparer(CrashAnalysisContext context)
{
private static readonly HashSet<string> KnownGameLogNames = new(StringComparer.OrdinalIgnoreCase)
{
"latest.log",
"latest log.txt",
"游戏崩溃前的输出.txt",
"rawoutput.log"
};
private static readonly HashSet<string> KnownDebugLogNames = new(StringComparer.OrdinalIgnoreCase)
{
"debug.log",
"debug log.txt"
};
private static readonly HashSet<string> KnownLauncherLogNames = new(StringComparer.OrdinalIgnoreCase)
{
"启动器日志.txt",
"PCL2 启动器日志.txt",
"PCL 启动器日志.txt",
"PCL CE 启动器日志.txt",
"log1.txt",
"log-ce1.log"
};
public bool Prepare()
{
LogWrapper.Info("Crash", "步骤 2:准备日志文本");
context.DirectOpenFile = null;
context.PreparedLogs = null;
context.OutputFiles.Clear();
var classifiedFiles = _ClassifyFiles();
var analyzable = classifiedFiles
.Where(item => item.Kind is not null)
.ToList();
context.DirectOpenFile = analyzable
.OrderBy(item => _IsGeneratedLog(item.File))
.ThenByDescending(item => _GetLastWriteTime(item.File))
.Select(item => item.File)
.FirstOrDefault();
var extraFiles = classifiedFiles
.Where(item => item.Kind is null)
.Select(item => item.File)
.ToList();
if (analyzable.Count == 0 && extraFiles.Count > 0)
{
LogWrapper.Info("Crash", "由于仅发现了额外日志,将它们视作 Minecraft 日志进行分析");
analyzable = extraFiles
.Select(file => new ClassifiedCrashLog(file, CrashLogKind.Game))
.ToList();
extraFiles.Clear();
}
var game = _SelectGameLog(analyzable);
var debug = _SelectDebugLog(analyzable);
var crashReport = _SelectNewest(analyzable, CrashLogKind.CrashReport, 300, 700);
var hsErr = _SelectNewest(analyzable, CrashLogKind.HsErr, 200, 100);
foreach (var extraFile in extraFiles)
{
context.OutputFiles.Add(extraFile.FullPath);
LogWrapper.Info("Crash", $"输出报告:{extraFile.FullPath},不用作分析");
}
var logs = new CrashLogSet
{
Game = game,
Debug = debug,
CrashReport = crashReport,
HsErr = hsErr,
All = string.Concat(
game?.Text ?? debug?.Text ?? string.Empty,
hsErr?.Text ?? string.Empty,
crashReport?.Text ?? string.Empty)
};
context.PreparedLogs = logs;
context.LogAll = logs.All;
if (logs.HasAnalyzableLog)
LogWrapper.Info(
"Crash",
("步骤 2:准备日志文本完成,找到" +
(game is null ? "" : "游戏日志、") +
(debug is null ? "" : "游戏 Debug 日志、") +
(hsErr is null ? "" : "虚拟机日志、") +
(crashReport is null ? "" : "崩溃日志、"))
.TrimEnd('、') + "用作分析");
else
LogWrapper.Info("Crash", "步骤 2:准备日志文本完成,没有任何可供分析的日志");
return logs.HasAnalyzableLog;
}
private List<ClassifiedCrashLog> _ClassifyFiles()
{
var result = new List<ClassifiedCrashLog>();
foreach (var file in context.RawFiles)
{
var name = file.FileName.ToLowerInvariant();
if (!file.Lines.Any())
{
LogWrapper.Info("Crash", $"{name} 由于内容为空跳过");
continue;
}
var kind = _ClassifyLogKind(file);
if (kind is null && !_IsExtraLogLike(file))
{
LogWrapper.Info("Crash", $"{name} 分类为 Ignore");
continue;
}
result.Add(new ClassifiedCrashLog(file, kind));
LogWrapper.Info("Crash", $"{name} 分类为 {kind?.ToString() ?? "Extra"}");
}
return result;
}
private static CrashLogKind? _ClassifyLogKind(CrashLogEntry file)
{
var name = file.FileName.ToLowerInvariant();
if (name.StartsWith("hs_err", StringComparison.Ordinal))
return CrashLogKind.HsErr;
if (name.StartsWith("crash-", StringComparison.Ordinal))
return CrashLogKind.CrashReport;
if (KnownDebugLogNames.Contains(name))
return CrashLogKind.Debug;
if (KnownGameLogNames.Contains(name))
return CrashLogKind.Game;
if (KnownLauncherLogNames.Contains(name) &&
file.Lines.Any(line => line.Contains("以下为游戏输出的最后一段内容")))
return CrashLogKind.Game;
return null;
}
private static bool _IsExtraLogLike(CrashLogEntry file)
{
var name = file.FileName.ToLowerInvariant();
return name.EndsWith(".log", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase);
}
private CrashLogText? _SelectGameLog(IReadOnlyList<ClassifiedCrashLog> files)
{
var gameFiles = files
.Where(item => item.Kind is CrashLogKind.Game or CrashLogKind.Debug)
.Select(item => item.File)
.ToList();
if (gameFiles.Count == 0)
return null;
var byName = gameFiles
.GroupBy(file => file.FileName.ToLowerInvariant())
.ToDictionary(
group => group.Key,
group => group.OrderBy(_GetLastWriteTime).Last(),
StringComparer.OrdinalIgnoreCase);
var text = "";
foreach (var name in new[]
{
"rawoutput.log",
"启动器日志.txt",
"log1.txt",
"log-ce1.log",
"游戏崩溃前的输出.txt",
"PCL CE 启动器日志.txt",
"PCL2 启动器日志.txt",
"PCL 启动器日志.txt"
})
{
if (!byName.TryGetValue(name, out var currentLog))
continue;
text += _ExtractLauncherGameOutput(currentLog);
context.OutputFiles.Add(currentLog.FullPath);
LogWrapper.Info("Crash", $"导入分析:{currentLog.FullPath},作为启动器日志");
break;
}
foreach (var name in new[]
{
"latest.log",
"latest log.txt",
"debug.log",
"debug log.txt"
})
{
if (!byName.TryGetValue(name, out var currentLog))
continue;
text += _GetHeadTailLines(currentLog.Lines, 1500, 500);
context.OutputFiles.Add(currentLog.FullPath);
LogWrapper.Info("Crash", $"导入分析:{currentLog.FullPath},作为 Minecraft 日志");
break;
}
if (string.IsNullOrEmpty(text))
{
var fallback = gameFiles
.OrderBy(_GetLastWriteTime)
.Last();
text = _GetHeadTailLines(fallback.Lines, 1500, 500);
context.OutputFiles.Add(fallback.FullPath);
LogWrapper.Info("Crash", $"导入分析:{fallback.FullPath},作为兜底日志");
}
foreach (var file in gameFiles.Where(file => !context.OutputFiles.Contains(file.FullPath)))
{
context.OutputFiles.Add(file.FullPath);
LogWrapper.Info("Crash", $"输出报告:{file.FullPath},作为 Minecraft 或启动器日志");
}
return new CrashLogText
{
Kind = CrashLogKind.Game,
Text = text.TrimEnd('\r', '\n'),
FilePath = gameFiles.FirstOrDefault()?.FullPath
};
}
private CrashLogText? _SelectDebugLog(IReadOnlyList<ClassifiedCrashLog> files)
{
var debugFiles = files
.Where(item => item.Kind == CrashLogKind.Debug)
.Select(item => item.File)
.ToList();
if (debugFiles.Count == 0)
return null;
var selected = debugFiles
.OrderBy(_GetLastWriteTime)
.Last();
if (!context.OutputFiles.Contains(selected.FullPath))
context.OutputFiles.Add(selected.FullPath);
LogWrapper.Info("Crash", $"导入分析:{selected.FullPath},作为 Minecraft Debug 日志");
return new CrashLogText
{
Kind = CrashLogKind.Debug,
Text = _GetHeadTailLines(selected.Lines, 1000, 0),
FilePath = selected.FullPath
};
}
private CrashLogText? _SelectNewest(
IReadOnlyList<ClassifiedCrashLog> files,
CrashLogKind kind,
int head,
int tail)
{
var selectedFiles = files
.Where(item => item.Kind == kind)
.Select(item => item.File)
.ToList();
if (selectedFiles.Count == 0)
return null;
var selected = selectedFiles
.OrderBy(_GetLastWriteTime)
.Last();
context.OutputFiles.Add(selected.FullPath);
LogWrapper.Info(
"Crash",
$"输出报告:{selected.FullPath}{(kind == CrashLogKind.HsErr ? "" : " Minecraft ")}");
LogWrapper.Info(
"Crash",
$"导入分析:{selected.FullPath}{(kind == CrashLogKind.HsErr ? "" : " Minecraft ")}");
return new CrashLogText
{
Kind = kind,
Text = _GetHeadTailLines(selected.Lines, head, tail),
FilePath = selected.FullPath
};
}
private static string _ExtractLauncherGameOutput(CrashLogEntry log)
{
var text = "";
var hasLauncherMark = false;
foreach (var line in log.Lines)
if (hasLauncherMark)
{
text += line + "\n";
}
else if (line.Contains("以下为游戏输出的最后一段内容"))
{
hasLauncherMark = true;
LogWrapper.Info("Crash", "找到 PCL 输出的游戏实时日志头");
}
return hasLauncherMark
? text.TrimEnd('\r', '\n')
: _GetHeadTailLines(log.Lines, 0, 500);
}
private static DateTime _GetLastWriteTime(CrashLogEntry file)
{
try
{
return new FileInfo(file.FullPath).LastWriteTime;
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "Crash", "获取日志文件修改时间失败");
return new DateTime(1900, 1, 1);
}
}
private bool _IsGeneratedLog(CrashLogEntry file) =>
file.FullPath.StartsWith(context.TempFolder, StringComparison.OrdinalIgnoreCase);
private static string _GetHeadTailLines(
IReadOnlyList<string> raw,
int headLines,
int tailLines)
{
if (raw.Count <= headLines + tailLines)
return string.Join("\n", raw.Distinct());
var lines = new List<string>();
var seen = new HashSet<string>();
var realHeadLines = 0;
int viewedLines;
for (viewedLines = 0; viewedLines <= raw.Count - 1; viewedLines++)
{
if (!seen.Add(raw[viewedLines]))
continue;
realHeadLines += 1;
lines.Add(raw[viewedLines]);
if (realHeadLines >= headLines)
break;
}
var realTailLines = 0;
for (var i = raw.Count - 1; i >= viewedLines; i -= 1)
{
if (!seen.Add(raw[i]))
continue;
realTailLines += 1;
lines.Insert(realHeadLines, raw[i]);
if (realTailLines >= tailLines)
break;
}
var result = new StringBuilder();
foreach (var line in lines.Where(line => !string.IsNullOrEmpty(line)))
{
result.Append(line);
result.Append('\n');
}
return result.ToString();
}
private sealed class ClassifiedCrashLog(
CrashLogEntry file,
CrashLogKind? kind)
{
public CrashLogEntry File { get; } = file;
public CrashLogKind? Kind { get; } = kind;
}
}
@@ -0,0 +1,29 @@
namespace PCL;
internal sealed class CrashLogSet
{
public CrashLogText? Game { get; init; }
public CrashLogText? Debug { get; init; }
public CrashLogText? CrashReport { get; init; }
public CrashLogText? HsErr { get; init; }
public string All { get; init; } = string.Empty;
public bool HasAnalyzableLog => Game is not null || CrashReport is not null || HsErr is not null;
public string? GetText(CrashLogKind kind)
{
return kind switch
{
CrashLogKind.Game => Game?.Text,
CrashLogKind.Debug => Debug?.Text,
CrashLogKind.CrashReport => CrashReport?.Text,
CrashLogKind.HsErr => HsErr?.Text,
CrashLogKind.All => All,
_ => null
};
}
}
@@ -0,0 +1,10 @@
namespace PCL;
internal sealed class CrashLogText
{
public required CrashLogKind Kind { get; init; }
public required string Text { get; init; }
public string? FilePath { get; init; }
}
@@ -0,0 +1,189 @@
using System.Globalization;
using System.IO;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.UI;
namespace PCL;
internal sealed class CrashDialogPresenter(CrashAnalysisContext context)
{
private readonly CrashReportExporter _exporter = new();
private readonly CrashResultFormatter _formatter = new();
public void Output(
bool isHandAnalyze,
List<string>? extraFiles)
{
ModMain.frmMain.ShowWindowToTop();
var crashContent = _formatter.Format(context, isHandAnalyze);
var resultText = crashContent.Text;
var directFile = context.DirectOpenFile;
var openInstanceSettings =
context.Instance is not null &&
crashContent.SuggestedAction == CrashSuggestedAction.OpenInstanceSettings;
var title = isHandAnalyze
? Lang.Text("Crash.Dialog.Title.Manual")
: Lang.Text("Crash.Dialog.Title.Auto");
var secondButtonText = _GetSecondButtonText(
isHandAnalyze,
directFile,
openInstanceSettings);
var thirdButtonText = isHandAnalyze
? ""
: Lang.Text("Crash.Dialog.Button.ExportReport");
var secondButtonAction = _GetSecondButtonAction(
isHandAnalyze,
directFile,
openInstanceSettings);
var selectedButton = MsgBoxWrapper.ShowWithCustomButtons(
resultText,
title,
MsgBoxTheme.Info,
true,
new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"), 1),
new MsgBoxButtonInfo(secondButtonText, 2, secondButtonAction),
new MsgBoxButtonInfo(thirdButtonText, 3));
switch (selectedButton)
{
case 2:
if (openInstanceSettings)
_OpenModLoaderInstallPage();
else if (directFile is not null)
_OpenDirectFile(directFile);
break;
case 3:
_ExportReport(extraFiles);
break;
}
}
private static string _GetSecondButtonText(
bool isHandAnalyze,
CrashLogEntry? directFile,
bool openInstanceSettings)
{
if (isHandAnalyze || directFile is null)
return "";
return openInstanceSettings
? Lang.Text("Crash.Dialog.Button.GoToModify")
: Lang.Text("Crash.Dialog.Button.OpenLog");
}
private static Action? _GetSecondButtonAction(
bool isHandAnalyze,
CrashLogEntry? directFile,
bool openInstanceSettings)
{
if (isHandAnalyze ||
directFile is null ||
openInstanceSettings)
return null;
return () => _OpenDirectFile(directFile);
}
private void _OpenModLoaderInstallPage()
{
PageInstanceLeft.McInstance = context.Instance;
ModBase.RunInUi(() => ModMain.frmMain.PageChange(
FormMain.PageType.InstanceSetup,
FormMain.PageSubType.VersionInstall));
}
private static void _OpenDirectFile(CrashLogEntry directFile)
{
if (File.Exists(directFile.FullPath))
{
Basics.OpenPath(directFile.FullPath);
return;
}
var filePath = Path.Combine(Paths.Temp, "Crash.txt");
CrashFileIo.WriteText(filePath, string.Join("\r\n", directFile.Lines));
Basics.OpenPath(filePath);
}
private void _ExportReport(List<string>? extraFiles)
{
string? fileAddress = null;
try
{
fileAddress = _SelectReportSavePath();
if (string.IsNullOrEmpty(fileAddress))
return;
_exporter.Export(context, fileAddress, extraFiles);
HintWrapper.Show(
Lang.Text("Crash.Report.Export.Success"),
HintTheme.Success);
Basics.OpenPath(Path.GetDirectoryName(fileAddress) ?? fileAddress);
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Crash", "导出错误报告失败");
var message = _CreateExportFailureMessage(fileAddress, ex);
MsgBoxWrapper.ShowWithCustomButtons(
message,
Lang.Text("Crash.Export.Failed.Title"),
MsgBoxTheme.Error,
false,
new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"), 1),
new MsgBoxButtonInfo(
Lang.Text("Crash.Export.Failed.CopyDetails"),
2,
() => ModBase.ClipboardSet(message, false)));
}
}
private static string _CreateExportFailureMessage(
string? targetZipPath,
Exception exception)
{
var summary = string.IsNullOrWhiteSpace(targetZipPath)
? Lang.Text("Crash.Export.Failed.MessageWithoutPath")
: Lang.Text("Crash.Export.Failed.Message", targetZipPath);
return ExceptionDetails.Compose(summary, exception);
}
private static string? _SelectReportSavePath()
{
string? fileAddress = null;
ModBase.RunInUiWait(() => fileAddress = SystemDialogs.SelectSaveFile(
Lang.Text("Crash.Report.SaveDialog.Title"),
_GetDefaultReportFileName(),
Lang.Text("Crash.Report.SaveDialog.Filter")));
return fileAddress;
}
private static string _GetDefaultReportFileName()
{
var time = DateTime.Now
.ToString("G", CultureInfo.InvariantCulture)
.Replace("/", "-")
.Replace(":", ".")
.Replace(" ", "_");
return Lang.Text("Crash.Report.SaveDialog.DefaultFileName", time);
}
}
@@ -0,0 +1,38 @@
namespace PCL;
internal enum CrashSuggestedAction
{
None,
OpenInstanceSettings
}
internal sealed class CrashMessageSpec(
string reasonKey,
object?[] reasonArgs,
string? suggestionKey,
object?[] suggestionArgs,
bool appendReportHint,
bool appendHelpHint,
CrashSuggestedAction suggestedAction = CrashSuggestedAction.None)
{
public string ReasonKey { get; } = reasonKey;
public object?[] ReasonArgs { get; } = reasonArgs;
public string? SuggestionKey { get; } = suggestionKey;
public object?[] SuggestionArgs { get; } = suggestionArgs;
public bool AppendReportHint { get; } = appendReportHint;
public bool AppendHelpHint { get; } = appendHelpHint;
public CrashSuggestedAction SuggestedAction { get; } = suggestedAction;
}
internal sealed class CrashDialogContent(string text, CrashSuggestedAction suggestedAction)
{
public string Text { get; } = text;
public CrashSuggestedAction SuggestedAction { get; } = suggestedAction;
}
@@ -0,0 +1,525 @@
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.Utils;
using PCL.Core.Utils.Exts;
namespace PCL;
internal sealed class CrashResultFormatter
{
public CrashDialogContent Format(
CrashAnalysisContext context,
bool isHandAnalyze)
{
var result = context.Result ?? new CrashAnalysisResult();
if (!result.Any)
return _CreateUnknownContent(isHandAnalyze);
var specs = result.Findings
.Select(finding => _CreateMessageSpec(finding, context.LogAll))
.ToList();
var text = _JoinParagraphs(specs.Select(_Render));
if (!isHandAnalyze)
text = _AppendFollowUps(text, specs);
return new CrashDialogContent(
_NormalizeLineEndings(text).Trim('\r', '\n'),
specs[0].SuggestedAction);
}
private static CrashDialogContent _CreateUnknownContent(bool isHandAnalyze)
{
var spec = isHandAnalyze
? _Spec("Crash.Reason.Unknown.Manual")
: _Spec("Crash.Reason.Unknown.Auto", "Crash.Suggestion.ExportReport");
return new CrashDialogContent(
_NormalizeLineEndings(_Render(spec)).Trim('\r', '\n'),
CrashSuggestedAction.None);
}
private static string _AppendFollowUps(
string text,
IReadOnlyCollection<CrashMessageSpec> specs)
{
var followUps = new List<string>();
if (specs.Any(spec => spec.AppendReportHint))
followUps.Add(Lang.Text("Crash.Suggestion.ViewReport"));
if (specs.Any(spec => spec.AppendHelpHint))
followUps.Add(Lang.Text("Crash.Suggestion.ExportReport"));
var launcherOutdatedSuggestion = _GetLauncherOutdatedSuggestion();
if (!string.IsNullOrWhiteSpace(launcherOutdatedSuggestion))
followUps.Add(launcherOutdatedSuggestion);
return followUps.Count == 0
? text
: Lang.Text(
"Crash.Presentation.WithFollowUps",
text,
_JoinParagraphs(followUps));
}
private static string _Render(CrashMessageSpec spec)
{
var reason = Lang.Text(spec.ReasonKey, spec.ReasonArgs);
if (spec.SuggestionKey is null)
return Lang.Text("Crash.Presentation.ReasonOnly", reason);
return Lang.Text(
"Crash.Presentation.WithSuggestion",
reason,
Lang.Text(spec.SuggestionKey, spec.SuggestionArgs));
}
private static CrashMessageSpec _CreateMessageSpec(
CrashFinding finding,
string combinedLogText)
{
var additional = finding.Details.ToList();
switch (finding.Cause)
{
case CrashCause.ExtractedModFile:
return _Spec(
"Crash.Reason.ExtractedModFile",
"Crash.Suggestion.ExtractedModFile");
case CrashCause.OutOfMemory:
return _Spec(
"Crash.Reason.OutOfMemory",
"Crash.Suggestion.OutOfMemory",
help: true);
case CrashCause.UsingOpenJ9:
return _Spec(
"Crash.Reason.UsingOpenJ9",
"Crash.Suggestion.UsingOpenJ9");
case CrashCause.UsingJdk:
return _Spec(
"Crash.Reason.UsingJdk",
"Crash.Suggestion.UsingJdk");
case CrashCause.JavaTooNew:
return _Spec(
"Crash.Reason.JavaTooNew",
"Crash.Suggestion.JavaTooNew");
case CrashCause.JavaIncompatible:
return _Spec(
"Crash.Reason.JavaIncompatible",
"Crash.Suggestion.JavaIncompatible");
case CrashCause.InvalidModFileName:
return _Spec(
"Crash.Reason.InvalidModFileName",
"Crash.Suggestion.InvalidModFileName");
case CrashCause.MissingMixinBootstrap:
return _Spec(
"Crash.Reason.MissingMixinBootstrap",
"Crash.Suggestion.MissingMixinBootstrap");
case CrashCause.X86JavaMemoryLimit:
return Environment.Is64BitOperatingSystem
? _Spec(
"Crash.Reason.X86JavaMemoryLimit.OnX64Os",
"Crash.Suggestion.X86JavaMemoryLimit.OnX64Os")
: _Spec(
"Crash.Reason.X86JavaMemoryLimit.OnX86Os",
"Crash.Suggestion.X86JavaMemoryLimit.OnX86Os",
help: true);
case CrashCause.MissingDependencyOrWrongMcVersion:
return _FormatMissingDependency(additional);
case CrashCause.StackKeywordFound:
return additional.Count == 1
? _Spec(
"Crash.Reason.StackKeyword.Single",
"Crash.Suggestion.StackKeyword", _Args(additional[0]),
help: true)
: _Spec(
"Crash.Reason.StackKeyword.Multiple",
"Crash.Suggestion.StackKeyword",
_Args(_BulletList(additional)),
help: true);
case CrashCause.StackModNameFound or CrashCause.SuspectedModCrash:
return additional.Count == 1
? _Spec(
"Crash.Reason.SuspectedMod.Single",
"Crash.Suggestion.DisableThisMod",
_Args(additional[0]),
report: true,
help: true)
: _Spec(
"Crash.Reason.SuspectedMod.Multiple",
"Crash.Suggestion.DisableTheseMods",
_Args(_BulletList(additional)),
report: true,
help: true);
case CrashCause.ConfirmedModCrash:
return additional.Count == 1
? _Spec(
"Crash.Reason.ConfirmedMod.Single",
"Crash.Suggestion.DisableThisMod",
_Args(additional[0]),
report: true,
help: true)
: _Spec(
"Crash.Reason.ConfirmedMod.Multiple",
"Crash.Suggestion.DisableTheseMods",
_Args(_BulletList(additional)),
report: true,
help: true);
case CrashCause.ModMixinFailed:
if (additional.Count == 0)
return _Spec(
"Crash.Reason.ModMixinFailed.None",
"Crash.Suggestion.ModMixinFailed",
report: true,
help: true);
return additional.Count == 1
? _Spec(
"Crash.Reason.ModMixinFailed.Single",
"Crash.Suggestion.DisableThisMod",
_Args(additional[0]),
report: true,
help: true)
: _Spec(
"Crash.Reason.ModMixinFailed.Multiple",
"Crash.Suggestion.DisableTheseMods",
_Args(_BulletList(additional)),
report: true,
help: true);
case CrashCause.ModConfigCrash:
return additional.Count > 1 && additional[1] is not null
? _Spec(
"Crash.Reason.ModConfigCrash.WithConfig",
"Crash.Suggestion.ModConfigCrash",
_Args(additional[0], additional[1]))
: _Spec(
"Crash.Reason.ModConfigCrash.Simple",
"Crash.Suggestion.DisableThisMod",
_Args(additional.FirstOrDefault() ?? string.Empty),
report: true,
help: true);
case CrashCause.ModInitializationFailed:
return additional.Count == 1
? _Spec(
"Crash.Reason.ModInitializationFailed.Single",
"Crash.Suggestion.DisableThisMod",
_Args(additional[0]),
report: true,
help: true)
: _Spec(
"Crash.Reason.ModInitializationFailed.Multiple",
"Crash.Suggestion.DisableTheseMods",
_Args(_BulletList(additional)),
report: true,
help: true);
case CrashCause.SpecificBlockCrash:
return additional.Count == 1
? _Spec(
"Crash.Reason.SpecificBlock.Single",
"Crash.Suggestion.SpecificBlock.Single",
_Args(additional[0]),
help: true)
: _Spec(
"Crash.Reason.SpecificBlock.Multiple",
"Crash.Suggestion.SpecificBlock.Multiple",
help: true);
case CrashCause.DuplicateMods:
return additional.Count >= 2
? _Spec(
"Crash.Reason.DuplicateMods.Known",
"Crash.Suggestion.DuplicateMods",
_Args(_BulletList(additional)))
: _Spec(
"Crash.Reason.DuplicateMods.Unknown",
"Crash.Suggestion.DuplicateMods",
report: true,
help: true);
case CrashCause.SpecificEntityCrash:
return additional.Count == 1
? _Spec(
"Crash.Reason.SpecificEntity.Single",
"Crash.Suggestion.SpecificEntity.Single",
_Args(additional[0]),
help: true)
: _Spec(
"Crash.Reason.SpecificEntity.Multiple",
"Crash.Suggestion.SpecificEntity.Multiple",
help: true);
case CrashCause.OptiFineForgeIncompatible:
return _Spec(
"Crash.Reason.OptiFineForgeIncompatible",
"Crash.Suggestion.OptiFineForgeIncompatible");
case CrashCause.ShadersModWithOptiFine:
return _Spec(
"Crash.Reason.ShadersModWithOptiFine",
"Crash.Suggestion.ShadersModWithOptiFine");
case CrashCause.OldForgeNewJavaIncompatible:
return _Spec(
"Crash.Reason.OldForgeNewJavaIncompatible",
"Crash.Suggestion.OldForgeNewJavaIncompatible");
case CrashCause.MultipleForgeInInstanceJson:
return _Spec(
"Crash.Reason.MultipleForgeInInstanceJson",
"Crash.Suggestion.MultipleForgeInInstanceJson");
case CrashCause.ManualDebugCrash:
return _Spec("Crash.Reason.ManualDebugCrash");
case CrashCause.ModRequiresJava11:
return _Spec(
"Crash.Reason.ModRequiresJava11",
"Crash.Suggestion.ModRequiresJava11");
case CrashCause.VeryShortOutput:
return _Spec(
"Crash.Reason.VeryShortOutput",
"Crash.Suggestion.ExportReport",
_Args(additional.FirstOrDefault() ?? string.Empty));
case CrashCause.OptiFineWorldLoadCrash:
return _Spec(
"Crash.Reason.OptiFineWorldLoadCrash",
"Crash.Suggestion.OptiFineWorldLoadCrash",
help: true);
case CrashCause.PixelFormatNotSupported
or CrashCause.IntelDriverAccessViolation
or CrashCause.AmdDriverAccessViolation
or CrashCause.NvidiaDriverAccessViolation
or CrashCause.UnsupportedOpenGl:
return combinedLogText.Contains("hd graphics ")
? _Spec(
"Crash.Reason.GraphicsDriver.IntelOrIntegrated",
"Crash.Suggestion.GraphicsDriver.IntelOrIntegrated",
help: true)
: _Spec(
"Crash.Reason.GraphicsDriver.Generic",
"Crash.Suggestion.GraphicsDriver.Generic",
help: true);
case CrashCause.ResourcePackTooLarge:
return _Spec(
"Crash.Reason.ResourcePackTooLarge",
"Crash.Suggestion.ResourcePackTooLarge",
help: true);
case CrashCause.NightConfigBug:
return _Spec(
"Crash.Reason.NightConfigBug",
"Crash.Suggestion.NightConfigBug",
help: true);
case CrashCause.OpenGl1282:
return _Spec(
"Crash.Reason.OpenGl1282",
"Crash.Suggestion.OpenGl1282",
help: true);
case CrashCause.TooManyModsIdLimit:
return _Spec(
"Crash.Reason.TooManyModsIdLimit",
"Crash.Suggestion.TooManyModsIdLimit");
case CrashCause.FileOrContentValidationFailed:
return _Spec(
"Crash.Reason.FileOrContentValidationFailed",
"Crash.Suggestion.FileOrContentValidationFailed",
help: true);
case CrashCause.IncompleteForgeInstallation:
return _Spec(
"Crash.Reason.IncompleteForgeInstallation",
"Crash.Suggestion.IncompleteForgeInstallation",
help: true);
case CrashCause.FabricError:
return additional.Count == 1
? _Spec(
"Crash.Reason.FabricError.WithDetail",
"Crash.Suggestion.FollowLoaderInstructions",
_Args(additional[0]))
: _Spec(
"Crash.Reason.FabricError.Generic",
"Crash.Suggestion.FollowLoaderInstructions",
help: true);
case CrashCause.IncompatibleMods:
return _FormatIncompatibleMods(additional);
case CrashCause.ModLoaderError:
return additional.Count == 1
? _Spec(
"Crash.Reason.ModLoaderError.WithDetail",
"Crash.Suggestion.FollowLoaderInstructions",
_Args(additional[0]))
: _Spec(
"Crash.Reason.ModLoaderError.Generic",
"Crash.Suggestion.FollowLoaderInstructions",
help: true);
case CrashCause.FabricSolutionProvided:
return additional.Count == 1
? _Spec(
"Crash.Reason.FabricSolution.WithDetail",
"Crash.Suggestion.FollowLoaderInstructions",
_Args(additional[0]))
: _Spec(
"Crash.Reason.FabricSolution.Generic",
"Crash.Suggestion.FollowLoaderInstructions",
help: true);
case CrashCause.ForgeError:
return additional.Count == 1
? _Spec(
"Crash.Reason.ForgeError.WithDetail",
"Crash.Suggestion.FollowLoaderInstructions",
_Args(additional[0]))
: _Spec(
"Crash.Reason.ForgeError.Generic",
"Crash.Suggestion.FollowLoaderInstructions",
help: true);
case CrashCause.NoAnalyzableFile:
return _Spec(
"Crash.Reason.NoAnalyzableFile",
"Crash.Suggestion.ExportReport",
help: true);
default:
return _Spec(
"Crash.Reason.UnknownFinding",
"Crash.Suggestion.ExportReport",
_Args(finding.Cause.ToString()),
help: true);
}
}
private static CrashMessageSpec _FormatMissingDependency(List<string> additional)
{
if (additional.Count == 0)
return _Spec(
"Crash.Reason.MissingDependency.Generic",
"Crash.Suggestion.FollowLoaderInstructions",
help: true);
var info = _BulletList(additional);
return info.IsMatch(RegexPatterns.IncompatibleModLoaderErrorHint)
? _Spec(
"Crash.Reason.ModLoaderIncompatible",
"Crash.Suggestion.ModLoaderIncompatible",
_Args(info),
action: CrashSuggestedAction.OpenInstanceSettings)
: _Spec(
"Crash.Reason.MissingDependency.WithDetail",
"Crash.Suggestion.FollowLoaderInstructions",
_Args(info));
}
private static CrashMessageSpec _FormatIncompatibleMods(List<string> additional)
{
if (additional.Count != 1)
return _Spec(
"Crash.Reason.IncompatibleMods.Generic",
"Crash.Suggestion.FollowLoaderInstructions",
help: true);
var info = additional[0];
return info.IsMatch(RegexPatterns.IncompatibleModLoaderErrorHint)
? _Spec(
"Crash.Reason.ModLoaderIncompatible",
"Crash.Suggestion.ModLoaderIncompatible",
_Args(info),
action: CrashSuggestedAction.OpenInstanceSettings)
: _Spec(
"Crash.Reason.IncompatibleMods.WithDetail",
"Crash.Suggestion.FollowLoaderInstructions",
_Args(info));
}
private static CrashMessageSpec _Spec(
string reasonKey,
string? suggestionKey = null,
object?[]? reasonArgs = null,
object?[]? suggestionArgs = null,
bool report = false,
bool help = false,
CrashSuggestedAction action = CrashSuggestedAction.None)
{
return new CrashMessageSpec(
reasonKey,
reasonArgs ?? [],
suggestionKey,
suggestionArgs ?? [],
report,
help,
action);
}
private static object?[] _Args(params object?[] args)
{
return args;
}
private static string _BulletList(IEnumerable<string> items)
{
return string.Join(
Environment.NewLine,
items.Select(item => Lang.Text("Crash.Presentation.ListItem", item)));
}
private static string _JoinParagraphs(IEnumerable<string> items)
{
return string.Join(
Environment.NewLine + Environment.NewLine,
items.Where(item => !string.IsNullOrWhiteSpace(item)));
}
private static string _NormalizeLineEndings(string text)
{
return text
.Replace("\r\n", "\r")
.Replace("\n", "\r")
.Replace("\r", "\r\n");
}
private static string? _GetLauncherOutdatedSuggestion()
{
try
{
return UpdateManager.GetVersionStatus() == UpdateEnums.VersionStatus.Latest
? null
: Lang.Text("Crash.Suggestion.LauncherOutdated");
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Crash", "确认启动器更新失败");
return null;
}
}
}