974 lines
38 KiB
C#
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();
|
|
}
|
|
}
|