Files

974 lines
38 KiB
C#

using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace gregExtractor;
public sealed class MainForm : Form
{
private readonly TextBox _txtRepoRoot = new();
private readonly TextBox _txtSourceRoot = new();
private readonly TextBox _txtGameRoot = new();
private readonly TextBox _txtMelonGeneratedRoot = new();
private readonly TextBox _txtTemplateOutput = new();
private readonly TextBox _txtTemplatePluginName = new();
private readonly TextBox _txtSummary = new();
private readonly TextBox _txtLog = new();
private readonly DataGridView _gridHooks = new();
private readonly DataGridView _gridAssemblyCoverage = new();
private readonly ComboBox _cmbCategory = new();
private readonly Label _lblCoverage = new();
private readonly Label _lblGregCoreCoverage = new();
private readonly Label _lblAssemblyCoverage = new();
private readonly Button _btnScan = new();
private readonly Button _btnSync = new();
private readonly Button _btnRegenerate = new();
private readonly Button _btnBuild = new();
private readonly Button _btnImportGame = new();
private readonly Button _btnImportMelon = new();
private readonly Button _btnGenerateTemplate = new();
private readonly Button _btnHelp = new();
private readonly CheckBox _chkAutoRegenerate = new();
private readonly CheckBox _chkBuildAfterRegenerate = new();
private readonly CheckBox _chkWatch = new();
private readonly Label _lblStatus = new();
private readonly SnapshotStore _store;
private readonly HookAutomationService _automation;
private readonly SemaphoreSlim _operationLock = new(1, 1);
private readonly System.Windows.Forms.Timer _debounceTimer = new();
private readonly List<FileSystemWatcher> _watchers = new();
private List<HookCatalogRow> _allHookRows = new();
private volatile bool _pendingFileChange;
public MainForm()
{
string repoRoot = ResolveRepoRoot();
string sourceRoot = HookAutomationService.SuggestDefaultIl2CppAssembliesPath();
if (string.IsNullOrWhiteSpace(sourceRoot))
sourceRoot = Path.Combine(repoRoot, "gregReferences", "Assembly-CSharp");
string gameRoot = HookAutomationService.SuggestDefaultGameDirectory();
string melonGeneratedRoot = HookAutomationService.SuggestDefaultMelonGeneratedPath();
string templateOutput = Path.Combine(repoRoot, "gregExtractor", "generated-template");
string stateRoot = Path.Combine(repoRoot, "gregExtractor", "state");
_store = new SnapshotStore(stateRoot);
_automation = new HookAutomationService(new SourceScanner(), _store);
Text = "gregExtractor - Hook Sync GUI";
Width = 1180;
Height = 900;
StartPosition = FormStartPosition.CenterScreen;
BuildUi(repoRoot, sourceRoot, gameRoot, melonGeneratedRoot, templateOutput);
ConfigureWatcherDebounce();
AppendLog("gregExtractor gestartet.");
AppendLog($"RepoRoot: {repoRoot}");
AppendLog($"SourceRoot: {sourceRoot}");
if (!string.IsNullOrWhiteSpace(gameRoot))
AppendLog($"GameRoot: {gameRoot}");
if (!string.IsNullOrWhiteSpace(melonGeneratedRoot))
AppendLog($"MelonGeneratedRoot: {melonGeneratedRoot}");
RefreshHookRows(_store.TryLoadSnapshot());
DarkTheme.Apply(this);
}
private void BuildUi(string repoRoot, string sourceRoot, string gameRoot, string melonGeneratedRoot, string templateOutput)
{
MinimumSize = new System.Drawing.Size(1200, 860);
var panelTop = new GroupBox
{
Dock = DockStyle.Top,
Height = 320,
Text = "Import, Pfade und Aktionen",
Padding = new Padding(10),
};
Controls.Add(panelTop);
var topLayout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 4,
RowCount = 10,
Padding = new Padding(4),
};
topLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
topLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
topLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 150));
topLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 250));
panelTop.Controls.Add(topLayout);
for (int row = 0; row < topLayout.RowCount; row++)
topLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
void AddPathRow(int rowIndex, string labelText, TextBox textBox, string textValue, string browseText, EventHandler browseAction, Button? actionButton = null)
{
var label = new Label
{
Text = labelText,
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(4, 8, 4, 6),
};
textBox.Text = textValue;
textBox.Dock = DockStyle.Fill;
textBox.Margin = new Padding(4, 4, 8, 4);
var browseButton = new Button
{
Text = browseText,
Dock = DockStyle.Fill,
Margin = new Padding(4),
};
browseButton.Click += browseAction;
topLayout.Controls.Add(label, 0, rowIndex);
topLayout.Controls.Add(textBox, 1, rowIndex);
topLayout.Controls.Add(browseButton, 2, rowIndex);
if (actionButton != null)
{
actionButton.Dock = DockStyle.Fill;
actionButton.Margin = new Padding(4);
topLayout.Controls.Add(actionButton, 3, rowIndex);
}
}
_btnImportMelon.Text = "Melon importieren";
_btnImportMelon.Click += async (_, _) => await ImportFromMelonAsync();
_btnImportGame.Text = "Aus Spielordner importieren";
_btnImportGame.Click += async (_, _) => await ImportFromGameAsync();
AddPathRow(0, "Repo:", _txtRepoRoot, repoRoot, "Wählen", (_, _) => BrowseFolder(_txtRepoRoot));
AddPathRow(1, "Source:", _txtSourceRoot, sourceRoot, "Wählen", (_, _) => BrowseFolder(_txtSourceRoot));
AddPathRow(2, "Melon Generated:", _txtMelonGeneratedRoot, melonGeneratedRoot, "Wählen", (_, _) => BrowseFolder(_txtMelonGeneratedRoot), _btnImportMelon);
AddPathRow(3, "Game:", _txtGameRoot, gameRoot, "Wählen", (_, _) => BrowseFolder(_txtGameRoot), _btnImportGame);
AddPathRow(4, "Template Ziel:", _txtTemplateOutput, templateOutput, "Wählen", (_, _) => BrowseFolder(_txtTemplateOutput));
var pluginLabel = new Label
{
Text = "Plugin Name:",
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(4, 8, 4, 6),
};
topLayout.Controls.Add(pluginLabel, 0, 5);
_txtTemplatePluginName.Text = "greg.Plugin.HookTemplate";
_txtTemplatePluginName.Dock = DockStyle.Fill;
_txtTemplatePluginName.Margin = new Padding(4, 4, 8, 4);
topLayout.Controls.Add(_txtTemplatePluginName, 1, 5);
topLayout.SetColumnSpan(_txtTemplatePluginName, 2);
_btnGenerateTemplate.Text = "Plugin Template bauen (alle Hooks)";
_btnGenerateTemplate.Click += async (_, _) => await GenerateTemplateAsync();
_btnGenerateTemplate.Dock = DockStyle.Fill;
_btnGenerateTemplate.Margin = new Padding(4);
topLayout.Controls.Add(_btnGenerateTemplate, 3, 5);
var actionPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
Margin = new Padding(4, 6, 4, 2),
};
_btnScan.Text = "1) Nur Änderungen scannen";
_btnScan.AutoSize = true;
_btnScan.Click += async (_, _) => await ScanOnlyAsync();
_btnSync.Text = "2) Scan + bei Änderung syncen";
_btnSync.AutoSize = true;
_btnSync.Click += async (_, _) => await ScanAndSyncIfNeededAsync();
_btnRegenerate.Text = "Hooks regenerieren";
_btnRegenerate.AutoSize = true;
_btnRegenerate.Click += async (_, _) => await RegenerateHooksAsync(buildAfter: _chkBuildAfterRegenerate.Checked);
_btnBuild.Text = "gregCore builden";
_btnBuild.AutoSize = true;
_btnBuild.Click += async (_, _) => await BuildGregCoreAsync();
actionPanel.Controls.Add(_btnScan);
actionPanel.Controls.Add(_btnSync);
actionPanel.Controls.Add(_btnRegenerate);
actionPanel.Controls.Add(_btnBuild);
topLayout.Controls.Add(actionPanel, 0, 6);
topLayout.SetColumnSpan(actionPanel, 4);
var optionsPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
Margin = new Padding(4, 0, 4, 0),
};
_chkWatch.Text = "Dateien überwachen (continuous mode)";
_chkWatch.AutoSize = true;
_chkWatch.CheckedChanged += (_, _) => ToggleWatchers(_chkWatch.Checked);
_chkAutoRegenerate.Text = "Auto-Regenerate bei Änderungen";
_chkAutoRegenerate.AutoSize = true;
_chkAutoRegenerate.Checked = true;
_chkBuildAfterRegenerate.Text = "Nach Regeneration automatisch builden";
_chkBuildAfterRegenerate.AutoSize = true;
_chkBuildAfterRegenerate.Checked = true;
optionsPanel.Controls.Add(_chkWatch);
optionsPanel.Controls.Add(_chkAutoRegenerate);
optionsPanel.Controls.Add(_chkBuildAfterRegenerate);
topLayout.Controls.Add(optionsPanel, 0, 7);
topLayout.SetColumnSpan(optionsPanel, 4);
_btnHelp.Text = "Help";
_btnHelp.AutoSize = true;
_btnHelp.Click += (_, _) => ShowHelpDialog();
topLayout.Controls.Add(_btnHelp, 3, 8);
_lblStatus.Text = "Status: Bereit";
_lblStatus.Dock = DockStyle.Fill;
_lblStatus.AutoEllipsis = true;
_lblStatus.Margin = new Padding(4, 4, 4, 0);
topLayout.Controls.Add(_lblStatus, 0, 9);
topLayout.SetColumnSpan(_lblStatus, 4);
var panelMain = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Horizontal,
SplitterDistance = 320,
};
Controls.Add(panelMain);
var topSplit = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical,
SplitterDistance = 470,
};
panelMain.Panel1.Controls.Add(topSplit);
var summaryGroup = new GroupBox
{
Dock = DockStyle.Fill,
Text = "Change Summary",
Padding = new Padding(8),
};
topSplit.Panel1.Controls.Add(summaryGroup);
_txtSummary.Dock = DockStyle.Fill;
_txtSummary.Multiline = true;
_txtSummary.ScrollBars = ScrollBars.Both;
_txtSummary.Font = new System.Drawing.Font("Consolas", 10f);
summaryGroup.Controls.Add(_txtSummary);
var hooksPanel = new GroupBox
{
Dock = DockStyle.Fill,
Text = "Hook Mapping & Coverage",
Padding = new Padding(8),
};
topSplit.Panel2.Controls.Add(hooksPanel);
var hookToolbar = new TableLayoutPanel
{
Dock = DockStyle.Top,
Height = 58,
ColumnCount = 4,
RowCount = 2,
Margin = new Padding(0, 0, 0, 6),
};
hookToolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 78));
hookToolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 210));
hookToolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
hookToolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 260));
hookToolbar.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
hookToolbar.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
hooksPanel.Controls.Add(hookToolbar);
var lblCategory = new Label { Text = "Kategorie:", Anchor = AnchorStyles.Left, AutoSize = true };
hookToolbar.Controls.Add(lblCategory, 0, 0);
_cmbCategory.Dock = DockStyle.Fill;
_cmbCategory.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbCategory.SelectedIndexChanged += (_, _) => ApplyHookFilter();
hookToolbar.Controls.Add(_cmbCategory, 1, 0);
_lblCoverage.Dock = DockStyle.Fill;
_lblCoverage.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
_lblCoverage.Text = "Katalog: n/a";
hookToolbar.Controls.Add(_lblCoverage, 2, 0);
hookToolbar.SetColumnSpan(_lblCoverage, 2);
var lblGregCore = new Label { Text = "gregCore:", Anchor = AnchorStyles.Left, AutoSize = true };
hookToolbar.Controls.Add(lblGregCore, 0, 1);
_lblGregCoreCoverage.Dock = DockStyle.Fill;
_lblGregCoreCoverage.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
_lblGregCoreCoverage.Text = "Umsetzung: n/a";
hookToolbar.Controls.Add(_lblGregCoreCoverage, 1, 1);
hookToolbar.SetColumnSpan(_lblGregCoreCoverage, 2);
_lblAssemblyCoverage.Dock = DockStyle.Fill;
_lblAssemblyCoverage.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
_lblAssemblyCoverage.Text = "Assemblies: n/a";
hookToolbar.Controls.Add(_lblAssemblyCoverage, 3, 1);
var hookSplit = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Horizontal,
SplitterDistance = 165,
};
hooksPanel.Controls.Add(hookSplit);
_gridHooks.Dock = DockStyle.Fill;
_gridHooks.AllowUserToAddRows = false;
_gridHooks.AllowUserToDeleteRows = false;
_gridHooks.ReadOnly = true;
_gridHooks.AutoGenerateColumns = false;
_gridHooks.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridHooks.MultiSelect = false;
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(HookCatalogRow.Il2CppHookEvent),
HeaderText = "IL2CPP Hook Event",
Width = 180,
});
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(HookCatalogRow.GregApiCall),
HeaderText = "greg API Call",
Width = 260,
});
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(HookCatalogRow.Category),
HeaderText = "Kategorie",
Width = 120,
});
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(HookCatalogRow.PatchTarget),
HeaderText = "Patch Target",
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
});
hookSplit.Panel1.Controls.Add(_gridHooks);
_gridAssemblyCoverage.Dock = DockStyle.Fill;
_gridAssemblyCoverage.AllowUserToAddRows = false;
_gridAssemblyCoverage.AllowUserToDeleteRows = false;
_gridAssemblyCoverage.ReadOnly = true;
_gridAssemblyCoverage.AutoGenerateColumns = false;
_gridAssemblyCoverage.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridAssemblyCoverage.MultiSelect = false;
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(AssemblyCoverageRow.Assembly),
HeaderText = "Assembly",
Width = 180,
});
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(AssemblyCoverageRow.CoveragePercent),
HeaderText = "Coverage %",
Width = 90,
DefaultCellStyle = new DataGridViewCellStyle { Format = "F2" },
});
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(AssemblyCoverageRow.CoveredUnique),
HeaderText = "Covered",
Width = 80,
});
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(AssemblyCoverageRow.ExpectedUnique),
HeaderText = "Expected",
Width = 80,
});
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(AssemblyCoverageRow.MissingUnique),
HeaderText = "Missing",
Width = 80,
});
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(AssemblyCoverageRow.HookedUnique),
HeaderText = "Hooked",
Width = 80,
});
hookSplit.Panel2.Controls.Add(_gridAssemblyCoverage);
var logGroup = new GroupBox
{
Dock = DockStyle.Fill,
Text = "Log",
Padding = new Padding(8),
};
panelMain.Panel2.Controls.Add(logGroup);
_txtLog.Dock = DockStyle.Fill;
_txtLog.Multiline = true;
_txtLog.ScrollBars = ScrollBars.Both;
_txtLog.Font = new System.Drawing.Font("Consolas", 10f);
logGroup.Controls.Add(_txtLog);
}
private static string ResolveRepoRoot()
{
string current = AppContext.BaseDirectory;
DirectoryInfo? dir = new DirectoryInfo(current);
while (dir != null)
{
bool hasGregCore = Directory.Exists(Path.Combine(dir.FullName, "gregCore"));
bool hasRefs = Directory.Exists(Path.Combine(dir.FullName, "gregReferences"));
if (hasGregCore && hasRefs)
return dir.FullName;
dir = dir.Parent;
}
return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
}
private void BrowseFolder(TextBox target)
{
using var dialog = new FolderBrowserDialog
{
SelectedPath = target.Text,
UseDescriptionForTitle = true,
Description = "Ordner auswählen",
};
if (dialog.ShowDialog(this) == DialogResult.OK)
target.Text = dialog.SelectedPath;
}
private void ConfigureWatcherDebounce()
{
_debounceTimer.Interval = 2500;
_debounceTimer.Tick += async (_, _) =>
{
_debounceTimer.Stop();
if (!_pendingFileChange)
return;
_pendingFileChange = false;
await ScanAndSyncIfNeededAsync();
};
}
private void ToggleWatchers(bool enable)
{
foreach (FileSystemWatcher watcher in _watchers)
watcher.Dispose();
_watchers.Clear();
if (!enable)
{
AppendLog("Watcher deaktiviert.");
return;
}
string sourceRoot = _txtSourceRoot.Text.Trim();
if (!Directory.Exists(sourceRoot))
{
AppendLog("Watcher konnte nicht gestartet werden: SourceRoot nicht gefunden.");
_chkWatch.Checked = false;
return;
}
string[] directories = 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();
foreach (string dir in directories)
{
var watcher = new FileSystemWatcher(dir, "*.cs")
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = true,
};
watcher.Changed += OnWatchedFileChanged;
watcher.Created += OnWatchedFileChanged;
watcher.Deleted += OnWatchedFileChanged;
watcher.Renamed += (_, _) => OnWatchedFileChanged(null, null!);
_watchers.Add(watcher);
}
AppendLog($"Watcher aktiv: {directories.Length} Source-Ordner.");
}
private void OnWatchedFileChanged(object? sender, FileSystemEventArgs args)
{
_pendingFileChange = true;
_debounceTimer.Stop();
_debounceTimer.Start();
}
private async Task ScanOnlyAsync()
{
await RunExclusiveAsync(async () =>
{
string sourceRoot = _txtSourceRoot.Text.Trim();
SourceSnapshot current = _automation.ScanCurrent(sourceRoot);
ChangeReport report = _automation.CompareWithPrevious(current);
_automation.Persist(current, report);
SetSummary(report, current);
RefreshHookRows(current);
AppendLog($"Scan fertig. Methods: {current.Methods.Count}, Added: {report.Added}, Removed: {report.Removed}, SignatureChanged: {report.SignatureChanged}, BodyChanged: {report.BodyChanged}");
}, "Scanne Änderungen...");
}
private async Task ScanAndSyncIfNeededAsync()
{
await RunExclusiveAsync(async () =>
{
await ScanAndSyncCoreAsync();
}, "Scanne + synchronisiere...");
}
private async Task ImportFromMelonAsync()
{
await RunExclusiveAsync(async () =>
{
string melonRoot = _txtMelonGeneratedRoot.Text.Trim();
string sourceRoot = _txtSourceRoot.Text.Trim();
MelonImportResult result = _automation.ImportMelonGeneratedSources(melonRoot, sourceRoot);
AppendLog($"Melon-Import fertig. Dirs: {result.CopiedDirectories}, Files: {result.CopiedFiles}");
AppendLog($"Quelle: {result.SourceRootUsed}");
AppendLog($"Ziel : {result.TargetRoot}");
await ScanAndSyncCoreAsync();
}, "Importiere Melon-generated Dateien...");
}
private async Task ImportFromGameAsync()
{
await RunExclusiveAsync(async () =>
{
string gameRoot = _txtGameRoot.Text.Trim();
string sourceRoot = _txtSourceRoot.Text.Trim();
MelonImportResult result = _automation.ImportFromGameDirectory(gameRoot, sourceRoot);
_txtMelonGeneratedRoot.Text = result.SourceRootUsed;
AppendLog($"Game-Import fertig. Dirs: {result.CopiedDirectories}, Files: {result.CopiedFiles}");
AppendLog($"Spiel : {gameRoot}");
AppendLog($"Quelle: {result.SourceRootUsed}");
AppendLog($"Ziel : {result.TargetRoot}");
await ScanAndSyncCoreAsync();
}, "Importiere Daten direkt aus dem Spielordner...");
}
private async Task GenerateTemplateAsync()
{
await RunExclusiveAsync(async () =>
{
if (_allHookRows.Count == 0)
RefreshHookRows(_store.TryLoadSnapshot());
string repoRoot = _txtRepoRoot.Text.Trim();
string outputDir = _txtTemplateOutput.Text.Trim();
string pluginName = string.IsNullOrWhiteSpace(_txtTemplatePluginName.Text)
? "greg.Plugin.HookTemplate"
: _txtTemplatePluginName.Text.Trim();
string rootNamespace = pluginName.Replace('-', '_').Replace(' ', '_');
string className = BuildClassNameFromPluginName(pluginName);
(string csprojPath, string mainCsPath, string readmePath) = _automation.GeneratePluginTemplate(
repoRoot: repoRoot,
outputDirectory: outputDir,
pluginName: pluginName,
rootNamespace: rootNamespace,
className: className,
author: "gregExtractor",
rows: _allHookRows);
AppendLog("Plugin-Template erzeugt:");
AppendLog($"- {csprojPath}");
AppendLog($"- {mainCsPath}");
AppendLog($"- {readmePath}");
await Task.CompletedTask;
}, "Erzeuge Plugin-Template...");
}
private async Task ScanAndSyncCoreAsync()
{
string sourceRoot = _txtSourceRoot.Text.Trim();
SourceSnapshot current = _automation.ScanCurrent(sourceRoot);
ChangeReport report = _automation.CompareWithPrevious(current);
_automation.Persist(current, report);
SetSummary(report, current);
RefreshHookRows(current);
if (!report.HasChanges)
{
AppendLog("Keine Änderungen erkannt.");
return;
}
AppendLog($"Änderungen erkannt: +{report.Added} / -{report.Removed} / SigΔ {report.SignatureChanged} / BodyΔ {report.BodyChanged}");
if (_chkAutoRegenerate.Checked)
{
AppendLog("Auto-Regenerate aktiv -> Generator wird gestartet.");
await RegenerateHooksCoreAsync(_chkBuildAfterRegenerate.Checked);
}
else
{
AppendLog("Auto-Regenerate ist aus. Nur Report aktualisiert.");
}
}
private async Task RegenerateHooksAsync(bool buildAfter)
{
await RunExclusiveAsync(async () =>
{
await RegenerateHooksCoreAsync(buildAfter);
}, "Regeneriere Hooks...");
}
private async Task RegenerateHooksCoreAsync(bool buildAfter)
{
string repoRoot = _txtRepoRoot.Text.Trim();
(int exitCode, string output) = await _automation.RunGeneratorAsync(repoRoot, CancellationToken.None);
AppendLog("--- Generator Output ---");
AppendLog(output);
if (exitCode != 0)
{
AppendLog($"Generator fehlgeschlagen (ExitCode={exitCode}).");
return;
}
AppendLog("Generator erfolgreich.");
RefreshHookRows(_store.TryLoadSnapshot());
if (buildAfter)
await BuildGregCoreCoreAsync();
}
private async Task BuildGregCoreAsync()
{
await RunExclusiveAsync(async () =>
{
await BuildGregCoreCoreAsync();
}, "Baue gregCore...");
}
private async Task BuildGregCoreCoreAsync()
{
string repoRoot = _txtRepoRoot.Text.Trim();
(int exitCode, string output) = await _automation.BuildGregCoreAsync(repoRoot, CancellationToken.None);
AppendLog("--- Build Output ---");
AppendLog(output);
if (exitCode == 0)
AppendLog("Build erfolgreich.");
else
AppendLog($"Build fehlgeschlagen (ExitCode={exitCode}).");
}
private async Task RunExclusiveAsync(Func<Task> action, string status)
{
if (!await _operationLock.WaitAsync(0))
{
AppendLog("Eine Operation läuft bereits.");
return;
}
try
{
SetBusyUi(true, status);
await action();
}
catch (Exception ex)
{
AppendLog($"FEHLER: {ex.Message}");
}
finally
{
SetBusyUi(false, "Bereit");
_operationLock.Release();
}
}
private void SetBusyUi(bool busy, string status)
{
_btnScan.Enabled = !busy;
_btnSync.Enabled = !busy;
_btnRegenerate.Enabled = !busy;
_btnBuild.Enabled = !busy;
_btnImportGame.Enabled = !busy;
_btnImportMelon.Enabled = !busy;
_btnGenerateTemplate.Enabled = !busy;
_btnHelp.Enabled = !busy;
_lblStatus.Text = $"Status: {status}";
}
private void SetSummary(ChangeReport report, SourceSnapshot snapshot)
{
var sb = new StringBuilder();
sb.AppendLine("gregExtractor - Change Summary");
sb.AppendLine("----------------------------------------------");
sb.AppendLine($"CreatedUtc : {report.CreatedUtc:yyyy-MM-dd HH:mm:ss} UTC");
sb.AppendLine($"SourceRoot : {snapshot.SourceRoot}");
sb.AppendLine($"Source Files : {snapshot.FileCount}");
sb.AppendLine($"Previous Method Count: {report.PreviousCount}");
sb.AppendLine($"Current Method Count : {report.CurrentCount}");
sb.AppendLine($"Added : {report.Added}");
sb.AppendLine($"Removed : {report.Removed}");
sb.AppendLine($"Signature Changed : {report.SignatureChanged}");
sb.AppendLine($"Body Changed : {report.BodyChanged}");
sb.AppendLine($"Has Changes : {report.HasChanges}");
sb.AppendLine();
sb.AppendLine("Hinweis:");
sb.AppendLine("- Signature Changed: API-/Signaturänderungen erkannt.");
sb.AppendLine("- Body Changed: Verhalten/Implementierung geändert, Signatur gleich.");
_txtSummary.Text = sb.ToString();
}
private void RefreshHookRows(SourceSnapshot? currentSnapshot)
{
try
{
string repoRoot = _txtRepoRoot.Text.Trim();
_allHookRows = _automation.LoadHookCatalogRows(repoRoot, _txtMelonGeneratedRoot.Text.Trim()).ToList();
List<HookCatalogRow> gregCoreRows = _automation.LoadGregCoreImplementedHookRows(repoRoot).ToList();
UpdateCategoryOptions();
ApplyHookFilter();
SourceSnapshot? snapshot = currentSnapshot ?? _store.TryLoadSnapshot();
if (snapshot == null)
{
_lblCoverage.Text = $"Katalog: n/a | Hooks: {_allHookRows.Count}";
_lblGregCoreCoverage.Text = $"Umsetzung: n/a | gregCore Hooks: {gregCoreRows.Count}";
_lblAssemblyCoverage.Text = "Assemblies: n/a";
_gridAssemblyCoverage.DataSource = null;
return;
}
HookCoverage catalogCoverage = _automation.CalculateCoverage(snapshot, _allHookRows);
_lblCoverage.Text = $"Katalog: {catalogCoverage.CoveragePercent:F2}% ({catalogCoverage.CoveredUnique}/{catalogCoverage.ExpectedUnique}) | Missing: {catalogCoverage.MissingUnique}";
HookCoverage gregCoreCoverage = _automation.CalculateCoverage(snapshot, gregCoreRows);
bool pluginReady = Math.Abs(gregCoreCoverage.CoveragePercent - 100d) < 0.0001;
string readyState = pluginReady ? "JA" : "NEIN";
_lblGregCoreCoverage.Text = $"Umsetzung: {gregCoreCoverage.CoveragePercent:F2}% ({gregCoreCoverage.CoveredUnique}/{gregCoreCoverage.ExpectedUnique}) | Missing: {gregCoreCoverage.MissingUnique} | Plugin-Ready: {readyState}";
List<AssemblyCoverageRow> assemblyRows = _automation.CalculateCoverageByAssembly(snapshot, gregCoreRows).ToList();
_gridAssemblyCoverage.DataSource = assemblyRows;
_lblAssemblyCoverage.Text = $"gregCore Assemblies: {assemblyRows.Count}";
}
catch (Exception ex)
{
_lblCoverage.Text = "Katalog: Fehler beim Laden";
_lblGregCoreCoverage.Text = "Umsetzung: Fehler";
_lblAssemblyCoverage.Text = "Assemblies: Fehler";
_gridAssemblyCoverage.DataSource = null;
AppendLog($"Hook-Tabelle konnte nicht geladen werden: {ex.Message}");
}
}
private void UpdateCategoryOptions()
{
string previous = _cmbCategory.SelectedItem?.ToString() ?? "Alle";
List<string> categories = _allHookRows
.Select(r => r.Category)
.Where(c => !string.IsNullOrWhiteSpace(c))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c, StringComparer.OrdinalIgnoreCase)
.ToList();
_cmbCategory.Items.Clear();
_cmbCategory.Items.Add("Alle");
foreach (string category in categories)
_cmbCategory.Items.Add(category);
int idx = _cmbCategory.Items.IndexOf(previous);
_cmbCategory.SelectedIndex = idx >= 0 ? idx : 0;
}
private void ApplyHookFilter()
{
string selected = _cmbCategory.SelectedItem?.ToString() ?? "Alle";
List<HookCatalogRow> filtered = string.Equals(selected, "Alle", StringComparison.OrdinalIgnoreCase)
? _allHookRows
: _allHookRows.Where(r => string.Equals(r.Category, selected, StringComparison.OrdinalIgnoreCase)).ToList();
_gridHooks.DataSource = filtered;
}
private static string BuildClassNameFromPluginName(string pluginName)
{
char[] separators = { '.', '-', ' ', '_', '/' };
string[] segments = pluginName
.Split(separators, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.ToArray();
if (segments.Length == 0)
return "GeneratedHookTemplate";
var sb = new StringBuilder();
foreach (string segment in segments)
{
if (segment.Length == 1)
sb.Append(char.ToUpperInvariant(segment[0]));
else
sb.Append(char.ToUpperInvariant(segment[0])).Append(segment[1..]);
}
if (!sb.ToString().EndsWith("Template", StringComparison.Ordinal))
sb.Append("Template");
return sb.ToString();
}
private void AppendLog(string text)
{
string line = $"[{DateTime.Now:HH:mm:ss}] {text}{Environment.NewLine}";
_txtLog.AppendText(line);
}
private void ShowHelpDialog()
{
using var helpForm = new Form
{
Text = "gregExtractor Help",
Width = 980,
Height = 760,
StartPosition = FormStartPosition.CenterParent,
MinimizeBox = false,
MaximizeBox = true,
};
var helpText = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
Font = new System.Drawing.Font("Consolas", 10f),
Text = BuildHelpText(),
};
helpForm.Controls.Add(helpText);
helpForm.ShowDialog(this);
}
private static string BuildHelpText()
{
var sb = new StringBuilder();
sb.AppendLine("gregExtractor - Hilfe");
sb.AppendLine("============================================================");
sb.AppendLine();
sb.AppendLine("Kurzantwort auf deine Frage:");
sb.AppendLine("- 'Melon Generated' ist NICHT der normale MelonLoader-Installationspfad alleine,");
sb.AppendLine(" sondern der Pfad, in dem die von Melon/Il2Cpp erzeugten C#-Quellen liegen.");
sb.AppendLine("- Dieser liegt oft im Spieleordner (z. B. <Game>\\MelonLoader\\Generated),");
sb.AppendLine(" kann aber je nach Setup auch unter %LocalAppData%\\MelonLoader liegen.");
sb.AppendLine("- 'Game' ist der Spieleordner selbst. Der Button 'Aus Spielordner importieren'");
sb.AppendLine(" sucht dort automatisch nach passenden Generated-Quellen.");
sb.AppendLine();
sb.AppendLine("Warum das für dich wichtig ist:");
sb.AppendLine("- Du musst nicht mehr jedes Update manuell mit DotPeek in ein Projekt umbauen.");
sb.AppendLine("- gregExtractor zieht die Daten direkt aus dem Spiel-/Melon-Output,");
sb.AppendLine(" erkennt Änderungen, regeneriert Hooks und zeigt den Umsetzungsstand in gregCore.");
sb.AppendLine();
sb.AppendLine("Felder & Buttons im oberen Bereich:");
sb.AppendLine("- Repo:");
sb.AppendLine(" Root deines Monorepos. Von hier werden Skripte/Builds aufgerufen.");
sb.AppendLine("- Source:");
sb.AppendLine(" Zielordner für extrahierte C#-Quellen (typisch gregReferences\\Assembly-CSharp).");
sb.AppendLine("- Melon Generated:");
sb.AppendLine(" Konkreter Generated-Quellpfad. Nutze 'Melon importieren', wenn du ihn direkt kennst.");
sb.AppendLine("- Game:");
sb.AppendLine(" Spieleverzeichnis. Nutze 'Aus Spielordner importieren', wenn gregExtractor suchen soll.");
sb.AppendLine("- Template Ziel + Plugin Name:");
sb.AppendLine(" Ziel und Name für ein Plugin-Template, das alle bekannten greg Hooks subscribed.");
sb.AppendLine();
sb.AppendLine("Aktionen:");
sb.AppendLine("- 1) Nur Änderungen scannen:");
sb.AppendLine(" Vergleicht aktuellen Source-Stand mit letztem Snapshot.");
sb.AppendLine("- 2) Scan + bei Änderung syncen:");
sb.AppendLine(" Scannt und triggert bei Änderungen automatisch Hook-Workflow.");
sb.AppendLine("- Hooks regenerieren:");
sb.AppendLine(" Führt den Hook-Generator aus (Generate-GregHooksFromIl2CppDump.ps1).");
sb.AppendLine("- gregCore builden:");
sb.AppendLine(" Baut gregCore, damit du sofort siehst ob alles compilebar ist.");
sb.AppendLine("- Plugin Template bauen:");
sb.AppendLine(" Erzeugt Vorlage für Plugin-Entwicklung gegen gregCore-Abhängigkeit.");
sb.AppendLine();
sb.AppendLine("Automations-Optionen:");
sb.AppendLine("- Dateien überwachen:");
sb.AppendLine(" Beobachtet Source-Ordner und stößt nach Änderungen (debounced) Sync an.");
sb.AppendLine("- Auto-Regenerate bei Änderungen:");
sb.AppendLine(" Bei erkanntem Delta startet automatisch die Regeneration.");
sb.AppendLine("- Nach Regeneration automatisch builden:");
sb.AppendLine(" Kette bis zum Build komplett durchlaufen lassen.");
sb.AppendLine();
sb.AppendLine("Hook/Coverage-Bereich (wie hilft das bei gregCore-Entwicklung):");
sb.AppendLine("- Katalog:");
sb.AppendLine(" Zeigt, wie viel vom erwarteten Snapshot im Hook-Katalog abgedeckt ist.");
sb.AppendLine("- gregCore Umsetzung:");
sb.AppendLine(" Zeigt, wie viel davon real in gregCore/framework/greg_hooks.json umgesetzt ist.");
sb.AppendLine("- Plugin-Ready: JA/NEIN:");
sb.AppendLine(" JA bedeutet: volle Abdeckung (100%) für die aktuelle Snapshot-Basis.");
sb.AppendLine(" Dann kann gregCore als stabile Abhängigkeit für Plugins genutzt werden.");
sb.AppendLine("- Assembly-Tabelle:");
sb.AppendLine(" Zeigt pro Assembly, wo noch Lücken (Missing) sind.");
sb.AppendLine();
sb.AppendLine("Empfohlener Workflow nach Game-Update:");
sb.AppendLine("1) Game-Pfad setzen.");
sb.AppendLine("2) 'Aus Spielordner importieren' klicken.");
sb.AppendLine("3) Scan/Sync + Regeneration laufen lassen (automatisch oder manuell).");
sb.AppendLine("4) 'gregCore Umsetzung' und Missing-Werte prüfen.");
sb.AppendLine("5) Bei 100% + Plugin-Ready=JA: Plugin-Template erzeugen und neue Features entwickeln.");
sb.AppendLine();
sb.AppendLine("Troubleshooting:");
sb.AppendLine("- Keine Quellen gefunden:");
sb.AppendLine(" Spiel einmal mit MelonLoader starten oder den Generated-Ordner direkt setzen.");
sb.AppendLine("- Import abgebrochen wegen Pfadüberschneidung:");
sb.AppendLine(" Source und Melon/Game-Pfad müssen getrennte Verzeichnisse sein.");
sb.AppendLine("- Coverage bleibt niedrig:");
sb.AppendLine(" Erst Regeneration + Build ausführen, dann erneut prüfen.");
return sb.ToString();
}
}