Files
gregExtractor/Services/FrameworkSyncService.cs
T

583 lines
22 KiB
C#

using gregExtractor.Models;
namespace gregExtractor.Services;
public sealed class FrameworkSyncService
{
private sealed class LineCounters
{
public int Added { get; set; }
public int Removed { get; set; }
}
private const string PatchMarker = "// [GREG_SYNC_INSERT_PATCHES]";
private const string EventIdsMarker = "// [GREG_SYNC_INSERT_EVENTIDS]";
private const string EventMapMarker = "// [GREG_SYNC_INSERT_EVENTIDS_MAP]";
private const string DtoMarker = "// [GREG_SYNC_INSERT_DTOS]";
private const string ApiMarker = "// [GREG_SYNC_INSERT_GAMEAPI]";
public SyncResult Sync(SyncOptions options)
{
if (options is null)
throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.FrameworkSourceDir) || !Directory.Exists(options.FrameworkSourceDir))
throw new DirectoryNotFoundException($"Framework source directory not found: {options.FrameworkSourceDir}");
var filesWritten = new List<string>();
var warnings = new List<string>();
var counters = new LineCounters();
string harmonyPath = FindFile(options.FrameworkSourceDir, "gregHarmonyPatches.cs");
string nativeHooksPath = FindFile(options.FrameworkSourceDir, "gregNativeEventHooks.cs");
string eventsPath = FindFile(options.FrameworkSourceDir, "Events.cs");
string gameApiPath = FindFile(options.FrameworkSourceDir, "gregGameApi.cs");
ProcessFile(harmonyPath, options, progressPrefix: "gregHarmonyPatches.cs", PatchMarker,
(content, now) => ApplyHarmonyChanges(content, options, now, counters),
filesWritten);
ProcessFile(nativeHooksPath, options, progressPrefix: "gregNativeEventHooks.cs", EventIdsMarker,
(content, now) => ApplyNativeHooksChanges(content, options, now, counters),
filesWritten);
ProcessFile(eventsPath, options, progressPrefix: "Events.cs", DtoMarker,
(content, now) => ApplyEventsChanges(content, options, now, counters),
filesWritten);
ProcessFile(gameApiPath, options, progressPrefix: "gregGameApi.cs", ApiMarker,
(content, now) => ApplyGameApiChanges(content, options, now, warnings, counters),
filesWritten);
return new SyncResult(filesWritten, counters.Added, counters.Removed, warnings);
}
private static string ApplyHarmonyChanges(string content, SyncOptions options, DateTime now, LineCounters counters)
{
content = EnsureMarker(content, PatchMarker);
foreach (HookDefinition hook in options.Diff.Added)
{
string className = GetPatchClassName(hook);
if (content.Contains($"internal static class {className}", StringComparison.Ordinal))
continue;
string snippet = BuildHarmonyPatchSnippet(hook, now);
content = InsertAtMarker(content, PatchMarker, snippet);
counters.Added += CountLines(snippet);
options.Progress?.Report($"+ [NEU] {DiffService.GetKey(hook)} → Patch hinzugefügt");
}
foreach (HookDefinition hook in options.Diff.Removed)
{
string className = GetPatchClassName(hook);
if (TryCommentOutClass(content, className, now, "Methode existiert nicht mehr in neuer Assembly-Version", out string updated, out int removedLines))
{
content = updated;
counters.Removed += removedLines;
options.Progress?.Report($"- [ENTFERNT] {DiffService.GetKey(hook)} → Patch auskommentiert");
}
}
foreach (HookDefinition hook in options.Diff.Changed)
{
string className = GetPatchClassName(hook);
if (TryUpdatePostfixSignature(content, className, hook, now, out string updated))
{
content = updated;
options.Progress?.Report($"~ [GEÄNDERT] {DiffService.GetKey(hook)} → Signatur aktualisiert");
}
}
return content;
}
private static string ApplyNativeHooksChanges(string content, SyncOptions options, DateTime now, LineCounters counters)
{
content = EnsureMarker(content, EventIdsMarker);
content = EnsureMarker(content, EventMapMarker);
int nextFreeId = GetNextFreeEventId(content);
foreach (HookDefinition hook in options.Diff.Added)
{
string eventName = GetEventIdName(hook);
if (!content.Contains($"public const int {eventName}", StringComparison.Ordinal))
{
string constSnippet = $" // [AUTO-GENERATED]{Environment.NewLine} public const int {eventName} = {nextFreeId};{Environment.NewLine}";
content = InsertAtMarker(content, EventIdsMarker, constSnippet);
counters.Added += CountLines(constSnippet);
nextFreeId++;
}
if (!content.Contains($"EventIds.{eventName}", StringComparison.Ordinal))
{
string mapSnippet = $" {{ EventIds.{eventName}, GregHookName.Create(\"{Escape(hook.Group)}\", \"{Escape(hook.MethodName)}\") }},{Environment.NewLine}";
content = InsertAtMarker(content, EventMapMarker, mapSnippet);
counters.Added += CountLines(mapSnippet);
}
}
foreach (HookDefinition hook in options.Diff.Removed)
{
string eventName = GetEventIdName(hook);
content = CommentMatchingLine(content, $"public const int {eventName}", $"// [DEPRECATED — {now:O}]", counters);
content = CommentMatchingLine(content, $"EventIds.{eventName}", $"// [DEPRECATED — {now:O}]", counters);
}
return content;
}
private static string ApplyEventsChanges(string content, SyncOptions options, DateTime now, LineCounters counters)
{
content = EnsureMarker(content, DtoMarker);
foreach (HookDefinition hook in options.Diff.Added)
{
if (hook.IsVoid && hook.Parameters.Count == 0)
continue;
string structName = GetDtoName(hook);
if (content.Contains($"public struct {structName}", StringComparison.Ordinal))
continue;
string dto = BuildDtoSnippet(hook, now);
content = InsertAtMarker(content, DtoMarker, dto);
counters.Added += CountLines(dto);
}
foreach (HookDefinition hook in options.Diff.Removed)
{
string structName = GetDtoName(hook);
if (TryCommentOutStruct(content, structName, now, out string updated, out int removedLines))
{
content = updated;
counters.Removed += removedLines;
}
}
return content;
}
private static string ApplyGameApiChanges(string content, SyncOptions options, DateTime now, List<string> warnings, LineCounters counters)
{
content = EnsureMarker(content, ApiMarker);
foreach (HookDefinition hook in options.Diff.Added.Where(IsCriticalHook))
{
string methodToken = $"{hook.MethodName}Delegate";
if (content.Contains(methodToken, StringComparison.Ordinal))
continue;
string snippet = BuildGameApiTodoSnippet(hook, now);
content = InsertAtMarker(content, ApiMarker, snippet);
counters.Added += CountLines(snippet);
warnings.Add($"⚠️ GameAPITable: {hook.MethodName} manuell prüfen + API_TABLE_VERSION erhöhen!");
}
return content;
}
private static string BuildHarmonyPatchSnippet(HookDefinition hook, DateTime now)
{
string className = GetPatchClassName(hook);
string parameterList = BuildPatchParameterList(hook);
string namespaceAndClass = $"{hook.Namespace}.{hook.ClassName}";
string eventName = GetEventIdName(hook);
return $"// [AUTO-GENERATED by gregExtractor sync — {now:yyyy-MM-dd}]\n" +
$"// Hook: {hook.Group} — Überprüfe Payload und aktiviere Event-Dispatch!\n" +
$"[HarmonyPatch(typeof({namespaceAndClass}),\n" +
$" nameof({namespaceAndClass}.{hook.MethodName}))]\n" +
$"internal static class {className}\n" +
"{\n" +
$" private static void Postfix({hook.ClassName} __instance{parameterList})\n" +
" {\n" +
" // TODO: Payload extrahieren und dispatchen\n" +
" // GregHookIntegration.EmitForSimple(\n" +
$" // EventIds.{eventName},\n" +
" // __instance\n" +
" // );\n" +
" }\n" +
"}\n\n";
}
private static string BuildDtoSnippet(HookDefinition hook, DateTime now)
{
string structName = GetDtoName(hook);
var builder = new StringBuilder();
builder.AppendLine($"// [AUTO-GENERATED by gregExtractor sync — {now:O}]");
builder.AppendLine("[StructLayout(LayoutKind.Sequential)]");
builder.AppendLine($"public struct {structName} : IModEvent");
builder.AppendLine("{");
builder.AppendLine(" public DateTime OccurredAtUtc { get; init; }");
foreach (HookParameter parameter in hook.Parameters)
{
string parameterName = ToPascalCase(parameter.Name);
string parameterType = NormalizeTypeName(parameter.Type);
builder.AppendLine($" public {parameterType} {parameterName} {{ get; init; }}");
}
builder.AppendLine("}");
builder.AppendLine();
return builder.ToString();
}
private static string BuildGameApiTodoSnippet(HookDefinition hook, DateTime now)
{
string delegateSignature = string.Join(", ", hook.Parameters.Select(x => $"{NormalizeTypeName(x.Type)} {ToCamelCase(x.Name)}"));
string fnName = ToCamelCase(hook.MethodName) + "Fn";
return $"// [AUTO-GENERATED — Version Bump erforderlich!]\n" +
"// WICHTIG: GameAPITable ist ABI-kritisch.\n" +
"// Neue Felder NUR ANS ENDE anhängen, niemals reordnen!\n" +
"// Nach dieser Änderung: API_TABLE_VERSION erhöhen!\n" +
$"// [GeneratedAt: {now:O}]\n" +
$"// public delegate {NormalizeTypeName(hook.ReturnType)} {hook.MethodName}Delegate({delegateSignature});\n" +
"// public IntPtr " + fnName + "; // Offset: TODO_nextOffset\n\n";
}
private static string EnsureMarker(string content, string marker)
{
if (content.Contains(marker, StringComparison.Ordinal))
return content;
return content.TrimEnd() + Environment.NewLine + Environment.NewLine + marker + Environment.NewLine;
}
private static string InsertAtMarker(string content, string marker, string snippet)
{
int markerIndex = content.IndexOf(marker, StringComparison.Ordinal);
if (markerIndex < 0)
return content + Environment.NewLine + snippet;
int insertIndex = markerIndex + marker.Length;
return content.Insert(insertIndex, Environment.NewLine + snippet);
}
private static int GetNextFreeEventId(string content)
{
MatchCollection matches = Regex.Matches(content, @"public\s+const\s+int\s+\w+\s*=\s*(\d+)\s*;", RegexOptions.CultureInvariant);
int max = 0;
foreach (Match match in matches)
{
if (!int.TryParse(match.Groups[1].Value, out int value))
continue;
max = Math.Max(max, value);
}
return max + 1;
}
private static string CommentMatchingLine(string content, string contains, string deprecationHeader, LineCounters counters)
{
string[] lines = content.Replace("\r", string.Empty, StringComparison.Ordinal).Split('\n');
bool changed = false;
for (int i = 0; i < lines.Length; i++)
{
if (!lines[i].Contains(contains, StringComparison.Ordinal))
continue;
if (lines[i].TrimStart().StartsWith("//", StringComparison.Ordinal))
continue;
lines[i] = "// " + lines[i];
counters.Removed++;
changed = true;
if (i == 0 || !lines[i - 1].Contains("[DEPRECATED", StringComparison.Ordinal))
{
lines[i] = deprecationHeader + Environment.NewLine + lines[i];
counters.Removed++;
}
}
return changed ? string.Join(Environment.NewLine, lines) : content;
}
private static bool TryCommentOutClass(string content, string className, DateTime now, string reason, out string updatedContent, out int removedLines)
{
return TryCommentOutTypeBlock(content, $"internal static class {className}",
$"// [DEPRECATED by gregExtractor sync — {now:O}]\n// {reason}",
out updatedContent,
out removedLines);
}
private static bool TryCommentOutStruct(string content, string structName, DateTime now, out string updatedContent, out int removedLines)
{
bool success = TryCommentOutTypeBlock(content, $"public struct {structName}",
$"[Obsolete(\"Removed in game version {now:O}\")]\n// [DEPRECATED by gregExtractor sync — {now:O}]",
out updatedContent,
out removedLines);
return success;
}
private static bool TryCommentOutTypeBlock(string content, string typeDeclarationNeedle, string header, out string updatedContent, out int removedLines)
{
updatedContent = content;
removedLines = 0;
int typeIndex = content.IndexOf(typeDeclarationNeedle, StringComparison.Ordinal);
if (typeIndex < 0)
return false;
int blockStart = FindBlockStart(content, typeIndex);
int braceStart = content.IndexOf('{', typeIndex);
if (braceStart < 0)
return false;
int blockEnd = FindMatchingBrace(content, braceStart);
if (blockEnd < 0)
return false;
string block = content.Substring(blockStart, blockEnd - blockStart + 1);
if (block.Contains("[DEPRECATED by gregExtractor sync", StringComparison.Ordinal))
return false;
string commentedBlock = string.Join(Environment.NewLine,
block.Replace("\r", string.Empty, StringComparison.Ordinal)
.Split('\n')
.Select(line => line.TrimStart().StartsWith("//", StringComparison.Ordinal) ? line : "// " + line));
removedLines += CountLines(block);
string replacement = header + Environment.NewLine + commentedBlock;
updatedContent = content.Remove(blockStart, block.Length).Insert(blockStart, replacement);
return true;
}
private static bool TryUpdatePostfixSignature(string content, string className, HookDefinition hook, DateTime now, out string updatedContent)
{
updatedContent = content;
int classIndex = content.IndexOf($"internal static class {className}", StringComparison.Ordinal);
if (classIndex < 0)
return false;
int classBraceStart = content.IndexOf('{', classIndex);
if (classBraceStart < 0)
return false;
int classBraceEnd = FindMatchingBrace(content, classBraceStart);
if (classBraceEnd < 0)
return false;
string classBlock = content.Substring(classIndex, classBraceEnd - classIndex + 1);
Match postfixMatch = Regex.Match(classBlock, @"private\s+static\s+void\s+Postfix\s*\(([^)]*)\)", RegexOptions.CultureInvariant);
if (!postfixMatch.Success)
return false;
string newSignature = $"{hook.ClassName} __instance{BuildPatchParameterList(hook)}";
if (string.Equals(postfixMatch.Groups[1].Value.Trim(), newSignature.Trim(), StringComparison.Ordinal))
return false;
string replacedBlock = classBlock.Replace(postfixMatch.Groups[1].Value, newSignature, StringComparison.Ordinal);
if (!replacedBlock.Contains("[SIGNATURE CHANGED", StringComparison.Ordinal))
{
replacedBlock = replacedBlock.Replace("private static void Postfix", $"// [SIGNATURE CHANGED — {now:O}] Bitte Payload prüfen!{Environment.NewLine} private static void Postfix", StringComparison.Ordinal);
}
updatedContent = content.Remove(classIndex, classBlock.Length).Insert(classIndex, replacedBlock);
return true;
}
private static int FindBlockStart(string content, int index)
{
int start = index;
while (start > 0)
{
int previousLineStart = content.LastIndexOf('\n', Math.Max(0, start - 2));
if (previousLineStart < 0)
break;
string line = content[(previousLineStart + 1)..start].Trim();
if (line.StartsWith("[", StringComparison.Ordinal) || line.StartsWith("//", StringComparison.Ordinal))
{
start = previousLineStart + 1;
continue;
}
break;
}
return start;
}
private static int FindMatchingBrace(string content, int openBraceIndex)
{
int depth = 0;
for (int i = openBraceIndex; i < content.Length; i++)
{
char current = content[i];
if (current == '{')
depth++;
else if (current == '}')
depth--;
if (depth == 0)
return i;
}
return -1;
}
private static void ProcessFile(
string filePath,
SyncOptions options,
string progressPrefix,
string requiredMarker,
Func<string, DateTime, string> transform,
List<string> filesWritten)
{
DateTime now = DateTime.UtcNow;
options.Progress?.Report($"> Verarbeite {progressPrefix}");
string content = File.ReadAllText(filePath);
string transformed = transform(content, now);
if (string.Equals(content, transformed, StringComparison.Ordinal))
{
options.Progress?.Report($"= Keine Änderungen in {progressPrefix}");
return;
}
if (options.DryRun)
{
options.Progress?.Report($"[DryRun] Würde {progressPrefix} schreiben");
filesWritten.Add(filePath + " (dry-run)");
return;
}
string backupPath = CreateBackup(filePath, now);
options.Progress?.Report($"> Backup erstellt: {backupPath}");
File.WriteAllText(filePath, transformed, Encoding.UTF8);
filesWritten.Add(filePath);
options.Progress?.Report($"✓ {progressPrefix} aktualisiert");
}
private static string CreateBackup(string filePath, DateTime now)
{
string backupPath = filePath + $".bak.{now:yyyyMMdd_HHmmss}";
File.Copy(filePath, backupPath, overwrite: true);
return backupPath;
}
private static string FindFile(string root, string fileName)
{
string? file = Directory
.EnumerateFiles(root, fileName, SearchOption.AllDirectories)
.FirstOrDefault();
if (string.IsNullOrWhiteSpace(file))
throw new FileNotFoundException($"Required framework file '{fileName}' not found under: {root}");
return file;
}
private static bool IsCriticalHook(HookDefinition hook)
{
string group = hook.Group ?? string.Empty;
string method = hook.MethodName ?? string.Empty;
return group.Contains("economy", StringComparison.OrdinalIgnoreCase)
|| group.Contains("persistence", StringComparison.OrdinalIgnoreCase)
|| group.Contains("hardware", StringComparison.OrdinalIgnoreCase)
|| method.Contains("save", StringComparison.OrdinalIgnoreCase)
|| method.Contains("load", StringComparison.OrdinalIgnoreCase)
|| method.Contains("coin", StringComparison.OrdinalIgnoreCase)
|| method.Contains("money", StringComparison.OrdinalIgnoreCase)
|| method.Contains("server", StringComparison.OrdinalIgnoreCase);
}
private static string BuildPatchParameterList(HookDefinition hook)
{
if (hook.Parameters.Count == 0)
return string.Empty;
return string.Concat(hook.Parameters.Select(parameter => $", {NormalizeTypeName(parameter.Type)} {ToCamelCase(parameter.Name)}"));
}
private static string GetPatchClassName(HookDefinition hook)
{
return SanitizeIdentifier($"{hook.ClassName}_{hook.MethodName}Patch");
}
private static string GetDtoName(HookDefinition hook)
{
return SanitizeIdentifier($"{hook.ClassName}_{hook.MethodName}Data");
}
private static string GetEventIdName(HookDefinition hook)
{
return SanitizeIdentifier($"{hook.Group}_{hook.MethodName}");
}
private static string NormalizeTypeName(string type)
{
return (type ?? "object").Trim();
}
private static string SanitizeIdentifier(string value)
{
var builder = new StringBuilder(value.Length);
foreach (char c in value)
{
if (char.IsLetterOrDigit(c) || c == '_')
builder.Append(c);
else
builder.Append('_');
}
if (builder.Length == 0)
return "GeneratedSymbol";
if (!char.IsLetter(builder[0]) && builder[0] != '_')
builder.Insert(0, '_');
return builder.ToString();
}
private static int CountLines(string text)
{
return text.Replace("\r", string.Empty, StringComparison.Ordinal).Split('\n').Length;
}
private static string ToPascalCase(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "Value";
string[] parts = value.Split(new[] { '_', '-', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
return "Value";
return string.Concat(parts.Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
}
private static string ToCamelCase(string value)
{
string pascal = ToPascalCase(value);
if (pascal.Length == 1)
return pascal.ToLowerInvariant();
return char.ToLowerInvariant(pascal[0]) + pascal[1..];
}
private static string Escape(string value)
{
return (value ?? string.Empty).Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal);
}
}
public sealed record SyncOptions(
string FrameworkSourceDir,
HookDiff Diff,
List<HookDefinition> AllHooks,
bool DryRun,
IProgress<string>? Progress);