CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
255 lines
8.8 KiB
C#
255 lines
8.8 KiB
C#
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", "");
|
||
}
|
||
} |