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

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

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

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

255 lines
8.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.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", "");
}
}