244 lines
8.1 KiB
C#
244 lines
8.1 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace gregExtractor;
|
|
|
|
public sealed class SourceScanner
|
|
{
|
|
private readonly Il2CppMetadataScanner _metadataScanner = new();
|
|
|
|
private static readonly Regex ClassRegex = new(
|
|
@"^\s*(?:public|private|protected|internal)\s+(?:sealed\s+)?class\s+(?<name>[A-Za-z0-9_]+)(\s*:\s*(?<base>[^\r\n{]+))?",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
|
|
private static readonly Regex MethodRegex = new(
|
|
@"^\s+(?:public|private|protected|internal)\s+(?<mods>(?:(?:static|unsafe|virtual|override|abstract|sealed|new|extern|partial|async)\s+)*)?(?<sig>.+?)\s*\((?<args>[^\)]*)\)\s*$",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
|
|
public SourceSnapshot Scan(string sourceRoot)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sourceRoot))
|
|
throw new ArgumentException("Source root is required.", nameof(sourceRoot));
|
|
|
|
if (TryResolveIl2CppAssembliesRoot(sourceRoot, out string? il2CppAssembliesRoot))
|
|
{
|
|
try
|
|
{
|
|
return _metadataScanner.Scan(il2CppAssembliesRoot!);
|
|
}
|
|
catch when (Directory.Exists(sourceRoot))
|
|
{
|
|
}
|
|
}
|
|
|
|
if (!Directory.Exists(sourceRoot))
|
|
throw new DirectoryNotFoundException($"Source root not found: {sourceRoot}");
|
|
|
|
var methodList = new List<MethodSnapshot>(capacity: 4096);
|
|
string[] sourceDirectories = Directory.GetDirectories(sourceRoot)
|
|
.Where(path =>
|
|
{
|
|
string name = Path.GetFileName(path);
|
|
return name.StartsWith("Il2Cpp", StringComparison.OrdinalIgnoreCase)
|
|
|| name.StartsWith("Unity", StringComparison.OrdinalIgnoreCase)
|
|
|| name.StartsWith("UnityEngine", StringComparison.OrdinalIgnoreCase);
|
|
})
|
|
.ToArray();
|
|
|
|
if (sourceDirectories.Length == 0)
|
|
throw new InvalidOperationException("No supported source directories found. Expected Il2Cpp*/Unity*/UnityEngine* folders or an Il2CppAssemblies DLL path.");
|
|
|
|
foreach (string directory in sourceDirectories)
|
|
{
|
|
string assemblyName = Path.GetFileName(directory);
|
|
foreach (string file in Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories))
|
|
{
|
|
ParseFile(file, assemblyName, methodList);
|
|
}
|
|
}
|
|
|
|
return new SourceSnapshot
|
|
{
|
|
CreatedUtc = DateTime.UtcNow,
|
|
SourceRoot = sourceRoot,
|
|
FileCount = sourceDirectories.Sum(d => Directory.EnumerateFiles(d, "*.cs", SearchOption.AllDirectories).Count()),
|
|
Methods = methodList
|
|
.GroupBy(m => m.SignatureKey, StringComparer.Ordinal)
|
|
.Select(g => g.First())
|
|
.OrderBy(m => m.SignatureKey, StringComparer.Ordinal)
|
|
.ToList(),
|
|
};
|
|
}
|
|
|
|
private static bool TryResolveIl2CppAssembliesRoot(string path, out string? il2CppAssembliesRoot)
|
|
{
|
|
il2CppAssembliesRoot = null;
|
|
|
|
string fullPath = Path.GetFullPath(path);
|
|
if (File.Exists(fullPath) && string.Equals(Path.GetFileName(fullPath), "Assembly-CSharp.dll", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
il2CppAssembliesRoot = Path.GetDirectoryName(fullPath)!;
|
|
return true;
|
|
}
|
|
|
|
if (!Directory.Exists(fullPath))
|
|
return false;
|
|
|
|
string[] candidateDirectories =
|
|
{
|
|
fullPath,
|
|
Path.Combine(fullPath, "MelonLoader", "Il2CppAssemblies"),
|
|
Path.Combine(fullPath, "Il2CppAssemblies"),
|
|
};
|
|
|
|
foreach (string candidate in candidateDirectories)
|
|
{
|
|
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "Assembly-CSharp.dll")))
|
|
{
|
|
il2CppAssembliesRoot = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void ParseFile(string filePath, string assemblyName, List<MethodSnapshot> output)
|
|
{
|
|
string? currentClass = null;
|
|
string[] lines = File.ReadAllLines(filePath);
|
|
|
|
for (int index = 0; index < lines.Length; index++)
|
|
{
|
|
string line = lines[index];
|
|
|
|
Match classMatch = ClassRegex.Match(line);
|
|
if (classMatch.Success)
|
|
{
|
|
currentClass = classMatch.Groups["name"].Value.Trim();
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(currentClass))
|
|
continue;
|
|
|
|
Match methodMatch = MethodRegex.Match(line);
|
|
if (!methodMatch.Success)
|
|
continue;
|
|
|
|
string signature = methodMatch.Groups["sig"].Value.Trim();
|
|
int spaceIndex = signature.LastIndexOf(' ');
|
|
if (spaceIndex <= 0)
|
|
continue;
|
|
|
|
string methodName = signature[(spaceIndex + 1)..].Trim();
|
|
string argList = methodMatch.Groups["args"].Value.Trim();
|
|
string patchSignature = BuildPatchSignature(currentClass, methodName, argList);
|
|
string bodyHash = ComputeMethodBodyHash(lines, index);
|
|
|
|
output.Add(new MethodSnapshot
|
|
{
|
|
Assembly = assemblyName,
|
|
TypeName = currentClass,
|
|
MethodName = methodName,
|
|
SignatureKey = $"{assemblyName}|{patchSignature}",
|
|
BodyHash = bodyHash,
|
|
});
|
|
}
|
|
}
|
|
|
|
private static string BuildPatchSignature(string className, string methodName, string argList)
|
|
{
|
|
List<string> types = new();
|
|
foreach (string segment in SplitArgSegments(argList))
|
|
{
|
|
string token = Regex.Replace(segment, @"\s*=\s*[^,)]+$", string.Empty).Trim();
|
|
if (token.Length == 0)
|
|
continue;
|
|
|
|
token = Regex.Replace(token, @"^\s*(ref|out|in|readonly)\s+", string.Empty).Trim();
|
|
string[] parts = token.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length < 2)
|
|
continue;
|
|
|
|
types.Add(string.Join(' ', parts[..^1]).Trim());
|
|
}
|
|
|
|
if (types.Count == 0)
|
|
return $"Il2Cpp.{className}::{methodName}()";
|
|
|
|
return $"Il2Cpp.{className}::{methodName}({string.Join(", ", types)})";
|
|
}
|
|
|
|
private static IEnumerable<string> SplitArgSegments(string argListText)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(argListText))
|
|
yield break;
|
|
|
|
int depth = 0;
|
|
StringBuilder current = new();
|
|
|
|
for (int i = 0; i < argListText.Length; i++)
|
|
{
|
|
char c = argListText[i];
|
|
if (c == '<') depth++;
|
|
if (c == '>') depth--;
|
|
|
|
if (c == ',' && depth == 0)
|
|
{
|
|
string segment = current.ToString().Trim();
|
|
if (segment.Length > 0)
|
|
yield return segment;
|
|
|
|
current.Clear();
|
|
continue;
|
|
}
|
|
|
|
current.Append(c);
|
|
}
|
|
|
|
if (current.Length > 0)
|
|
{
|
|
string segment = current.ToString().Trim();
|
|
if (segment.Length > 0)
|
|
yield return segment;
|
|
}
|
|
}
|
|
|
|
private static string ComputeMethodBodyHash(string[] lines, int methodLineIndex)
|
|
{
|
|
var sb = new StringBuilder();
|
|
int braceDepth = 0;
|
|
bool startedBody = false;
|
|
|
|
for (int i = methodLineIndex; i < lines.Length; i++)
|
|
{
|
|
string line = lines[i];
|
|
sb.AppendLine(line.Trim());
|
|
|
|
foreach (char c in line)
|
|
{
|
|
if (c == '{')
|
|
{
|
|
braceDepth++;
|
|
startedBody = true;
|
|
}
|
|
else if (c == '}')
|
|
{
|
|
braceDepth--;
|
|
}
|
|
}
|
|
|
|
if (startedBody && braceDepth <= 0)
|
|
break;
|
|
|
|
if (!startedBody && line.Contains(';'))
|
|
break;
|
|
}
|
|
|
|
byte[] data = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
|
|
return Convert.ToHexString(data);
|
|
}
|
|
}
|