Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Modules/Minecraft/CrashAnalysis/Detection/CrashModIndex.cs
T

255 lines
8.8 KiB
C#
Raw Normal View History

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", "");
}
}