Files

1593 lines
74 KiB
C#

using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace gregExtractor;
public sealed class MainFormV2 : Form
{
private enum UiLanguage
{
German,
English,
}
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 TextBox _txtHelp = new();
private readonly TextBox _txtModProjectPath = new();
private readonly TextBox _txtModAnalysisSummary = new();
private readonly TextBox _txtCoverageDetails = new();
private readonly TextBox _txtUsedHooks = new();
private readonly DataGridView _gridHooks = new();
private readonly DataGridView _gridAssemblyCoverage = new();
private readonly DataGridView _gridMigration = new();
private readonly DataGridView _gridFileInsights = new();
private readonly ComboBox _cmbCategory = new();
private readonly ComboBox _cmbLanguage = new();
private readonly Label _lblCoverage = new();
private readonly Label _lblGregCoreCoverage = new();
private readonly Label _lblAssemblyCoverage = new();
private readonly Label _lblStatus = new();
private readonly Label _lblTitle = new();
private readonly Label _lblLanguage = 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 _btnOpenTemplateOutput = new();
private readonly Button _btnBrowseModProject = new();
private readonly Button _btnAnalyzeModProject = new();
private readonly Button _btnOpenModProject = new();
private readonly CheckBox _chkAutoRegenerate = new();
private readonly CheckBox _chkBuildAfterRegenerate = new();
private readonly CheckBox _chkWatch = new();
private readonly SnapshotStore _store;
private readonly HookAutomationService _automation;
private readonly ModProjectAnalyzer _modAnalyzer = new();
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;
private UiLanguage _language = UiLanguage.German;
private string _lastGeneratedTemplateDirectory = string.Empty;
public MainFormV2()
{
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";
Width = 1400;
Height = 950;
MinimumSize = new System.Drawing.Size(1280, 860);
StartPosition = FormStartPosition.CenterScreen;
BuildUi(repoRoot, sourceRoot, gameRoot, melonGeneratedRoot, templateOutput);
ConfigureWatcherDebounce();
AppendLog(_language == UiLanguage.German ? "gregExtractor gestartet." : "gregExtractor started.");
AppendLog($"RepoRoot: {repoRoot}");
AppendLog($"SourceRoot: {sourceRoot}");
if (!string.IsNullOrWhiteSpace(gameRoot))
AppendLog($"GameRoot: {gameRoot}");
if (!string.IsNullOrWhiteSpace(melonGeneratedRoot))
AppendLog($"MelonGeneratedRoot: {melonGeneratedRoot}");
RefreshHookRows(_store.TryLoadSnapshot());
ApplyLanguage();
DarkTheme.Apply(this);
}
private void BuildUi(string repoRoot, string sourceRoot, string gameRoot, string melonGeneratedRoot, string templateOutput)
{
var headerPanel = new Panel
{
Dock = DockStyle.Top,
Height = 44,
Padding = new Padding(10, 8, 10, 6),
};
Controls.Add(headerPanel);
_lblTitle.AutoSize = true;
_lblTitle.Left = 10;
_lblTitle.Top = 12;
headerPanel.Controls.Add(_lblTitle);
_lblLanguage.AutoSize = true;
_lblLanguage.Top = 12;
_lblLanguage.Anchor = AnchorStyles.Top | AnchorStyles.Right;
headerPanel.Controls.Add(_lblLanguage);
_cmbLanguage.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbLanguage.Items.AddRange(new[] { "Deutsch", "English" });
_cmbLanguage.SelectedIndex = 0;
_cmbLanguage.Width = 120;
_cmbLanguage.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_cmbLanguage.SelectedIndexChanged += (_, _) =>
{
_language = _cmbLanguage.SelectedIndex == 1 ? UiLanguage.English : UiLanguage.German;
ApplyLanguage();
};
headerPanel.Controls.Add(_cmbLanguage);
headerPanel.Resize += (_, _) =>
{
_cmbLanguage.Left = headerPanel.Width - _cmbLanguage.Width - 12;
_cmbLanguage.Top = 8;
_lblLanguage.Left = _cmbLanguage.Left - _lblLanguage.Width - 8;
};
var statusPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 30,
Padding = new Padding(12, 6, 12, 4),
};
Controls.Add(statusPanel);
_lblStatus.Dock = DockStyle.Fill;
_lblStatus.AutoEllipsis = true;
statusPanel.Controls.Add(_lblStatus);
var tabs = new TabControl
{
Dock = DockStyle.Fill,
};
Controls.Add(tabs);
tabs.TabPages.Add(BuildWorkflowTab(repoRoot, sourceRoot, gameRoot, melonGeneratedRoot, templateOutput));
tabs.TabPages.Add(BuildCoverageTab());
tabs.TabPages.Add(BuildModAnalysisTab());
tabs.TabPages.Add(BuildLogTab());
tabs.TabPages.Add(BuildHelpTab());
}
private TabPage BuildWorkflowTab(string repoRoot, string sourceRoot, string gameRoot, string melonGeneratedRoot, string templateOutput)
{
var tab = new TabPage();
var split = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Horizontal,
SplitterDistance = 330,
};
tab.Controls.Add(split);
var pathsGroup = new GroupBox
{
Name = "WorkflowPathsGroup",
Dock = DockStyle.Fill,
Padding = new Padding(10),
};
split.Panel1.Controls.Add(pathsGroup);
var table = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 4,
RowCount = 9,
};
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 260));
for (int row = 0; row < table.RowCount; row++)
table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
pathsGroup.Controls.Add(table);
AddPathRow(table, 0, "PathRepoLabel", _txtRepoRoot, repoRoot, _ => BrowseFolder(_txtRepoRoot));
AddPathRow(table, 1, "PathSourceLabel", _txtSourceRoot, sourceRoot, _ => BrowseFolder(_txtSourceRoot));
_btnImportMelon.Click += async (_, _) => await ImportFromMelonAsync();
AddPathRow(table, 2, "PathMelonLabel", _txtMelonGeneratedRoot, melonGeneratedRoot, _ => BrowseFolder(_txtMelonGeneratedRoot), _btnImportMelon);
_btnImportGame.Click += async (_, _) => await ImportFromGameAsync();
AddPathRow(table, 3, "PathGameLabel", _txtGameRoot, gameRoot, _ => BrowseFolder(_txtGameRoot), _btnImportGame);
AddPathRow(table, 4, "PathTemplateLabel", _txtTemplateOutput, templateOutput, _ => BrowseFolder(_txtTemplateOutput));
var lblPlugin = new Label { Name = "PathPluginLabel", Text = "Plugin Name", Anchor = AnchorStyles.Left, AutoSize = true, Margin = new Padding(4, 8, 4, 4) };
table.Controls.Add(lblPlugin, 0, 5);
_txtTemplatePluginName.Text = "greg.Plugin.HookTemplate";
_txtTemplatePluginName.Dock = DockStyle.Fill;
_txtTemplatePluginName.Margin = new Padding(4);
table.Controls.Add(_txtTemplatePluginName, 1, 5);
table.SetColumnSpan(_txtTemplatePluginName, 2);
_btnGenerateTemplate.Dock = DockStyle.Fill;
_btnGenerateTemplate.Margin = new Padding(4);
_btnGenerateTemplate.Click += async (_, _) => await GenerateTemplateAsync();
table.Controls.Add(_btnGenerateTemplate, 3, 5);
var actionPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
AutoSize = true,
Margin = new Padding(4),
};
_btnScan.AutoSize = true;
_btnScan.Click += async (_, _) => await ScanOnlyAsync();
_btnSync.AutoSize = true;
_btnSync.Click += async (_, _) => await ScanAndSyncIfNeededAsync();
_btnRegenerate.AutoSize = true;
_btnRegenerate.Click += async (_, _) => await RegenerateHooksAsync(_chkBuildAfterRegenerate.Checked);
_btnBuild.AutoSize = true;
_btnBuild.Click += async (_, _) => await BuildGregCoreAsync();
_btnOpenTemplateOutput.AutoSize = true;
_btnOpenTemplateOutput.Enabled = false;
_btnOpenTemplateOutput.Click += (_, _) => OpenTemplateOutputFolder();
actionPanel.Controls.Add(_btnScan);
actionPanel.Controls.Add(_btnSync);
actionPanel.Controls.Add(_btnRegenerate);
actionPanel.Controls.Add(_btnBuild);
actionPanel.Controls.Add(_btnOpenTemplateOutput);
table.Controls.Add(actionPanel, 0, 6);
table.SetColumnSpan(actionPanel, 4);
var optionsPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
AutoSize = true,
Margin = new Padding(4),
};
_chkWatch.AutoSize = true;
_chkWatch.CheckedChanged += (_, _) => ToggleWatchers(_chkWatch.Checked);
_chkAutoRegenerate.AutoSize = true;
_chkAutoRegenerate.Checked = true;
_chkBuildAfterRegenerate.AutoSize = true;
_chkBuildAfterRegenerate.Checked = true;
optionsPanel.Controls.Add(_chkWatch);
optionsPanel.Controls.Add(_chkAutoRegenerate);
optionsPanel.Controls.Add(_chkBuildAfterRegenerate);
table.Controls.Add(optionsPanel, 0, 7);
table.SetColumnSpan(optionsPanel, 4);
var hintLabel = new Label
{
Dock = DockStyle.Fill,
AutoSize = true,
Margin = new Padding(4, 2, 4, 2),
Name = "WorkflowHintLabel",
};
table.Controls.Add(hintLabel, 0, 8);
table.SetColumnSpan(hintLabel, 4);
var summaryGroup = new GroupBox
{
Name = "WorkflowSummaryGroup",
Dock = DockStyle.Fill,
Padding = new Padding(8),
};
split.Panel2.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);
return tab;
}
private static void AddPathRow(TableLayoutPanel table, int row, string labelName, TextBox textBox, string initialText, Action<object?> onBrowse, Button? actionButton = null)
{
var label = new Label
{
Name = labelName,
Text = labelName,
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(4, 8, 4, 4),
};
table.Controls.Add(label, 0, row);
textBox.Text = initialText;
textBox.Dock = DockStyle.Fill;
textBox.Margin = new Padding(4);
table.Controls.Add(textBox, 1, row);
var btnBrowse = new Button
{
Name = labelName + "_Browse",
Dock = DockStyle.Fill,
Margin = new Padding(4),
Text = "Browse",
};
btnBrowse.Click += (_, arg) => onBrowse(arg);
table.Controls.Add(btnBrowse, 2, row);
if (actionButton != null)
{
actionButton.Dock = DockStyle.Fill;
actionButton.Margin = new Padding(4);
table.Controls.Add(actionButton, 3, row);
}
}
private TabPage BuildCoverageTab()
{
var tab = new TabPage();
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 2,
Padding = new Padding(8),
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
tab.Controls.Add(root);
var toolbar = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 6,
RowCount = 1,
Height = 36,
};
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 360));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 260));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220));
var lblCategory = new Label { Name = "CategoryLabel", AutoSize = true, Anchor = AnchorStyles.Left };
toolbar.Controls.Add(lblCategory, 0, 0);
_cmbCategory.Dock = DockStyle.Fill;
_cmbCategory.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbCategory.SelectedIndexChanged += (_, _) => ApplyHookFilter();
toolbar.Controls.Add(_cmbCategory, 1, 0);
_lblCoverage.Dock = DockStyle.Fill;
_lblCoverage.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
toolbar.Controls.Add(_lblCoverage, 2, 0);
_lblGregCoreCoverage.Dock = DockStyle.Fill;
_lblGregCoreCoverage.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
toolbar.Controls.Add(_lblGregCoreCoverage, 3, 0);
_lblAssemblyCoverage.Dock = DockStyle.Fill;
_lblAssemblyCoverage.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
toolbar.Controls.Add(_lblAssemblyCoverage, 4, 0);
var btnRefreshCoverage = new Button
{
Name = "RefreshCoverageButton",
Dock = DockStyle.Fill,
};
btnRefreshCoverage.Click += (_, _) => RefreshHookRows(_store.TryLoadSnapshot());
toolbar.Controls.Add(btnRefreshCoverage, 5, 0);
root.Controls.Add(toolbar, 0, 0);
var coverageTabs = new TabControl
{
Dock = DockStyle.Fill,
Name = "CoverageTabs",
};
root.Controls.Add(coverageTabs, 0, 1);
var overviewTab = new TabPage { Name = "CoverageOverviewTab" };
_txtCoverageDetails.Dock = DockStyle.Fill;
_txtCoverageDetails.Multiline = true;
_txtCoverageDetails.ReadOnly = true;
_txtCoverageDetails.ScrollBars = ScrollBars.Both;
_txtCoverageDetails.Font = new System.Drawing.Font("Consolas", 10f);
overviewTab.Controls.Add(_txtCoverageDetails);
coverageTabs.TabPages.Add(overviewTab);
var hooksTab = new TabPage { Name = "CoverageHooksTab" };
_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), Name = "HookEventColumn", Width = 240 });
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(HookCatalogRow.GregApiCall), Name = "GregApiColumn", Width = 320 });
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(HookCatalogRow.Category), Name = "CategoryColumn", Width = 160 });
_gridHooks.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(HookCatalogRow.PatchTarget), Name = "PatchTargetColumn", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill });
hooksTab.Controls.Add(_gridHooks);
coverageTabs.TabPages.Add(hooksTab);
var assemblyTab = new TabPage { Name = "CoverageAssembliesTab" };
_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), Name = "AssemblyColumn", Width = 220 });
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(AssemblyCoverageRow.CoveragePercent), Name = "CoveragePercentColumn", Width = 130, DefaultCellStyle = new DataGridViewCellStyle { Format = "F2" } });
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(AssemblyCoverageRow.CoveredUnique), Name = "CoveredColumn", Width = 100 });
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(AssemblyCoverageRow.ExpectedUnique), Name = "ExpectedColumn", Width = 100 });
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(AssemblyCoverageRow.MissingUnique), Name = "MissingColumn", Width = 100 });
_gridAssemblyCoverage.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(AssemblyCoverageRow.HookedUnique), Name = "HookedColumn", Width = 100 });
assemblyTab.Controls.Add(_gridAssemblyCoverage);
coverageTabs.TabPages.Add(assemblyTab);
return tab;
}
private TabPage BuildModAnalysisTab()
{
var tab = new TabPage();
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 2,
Padding = new Padding(8),
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
tab.Controls.Add(root);
var top = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 5,
RowCount = 1,
Height = 36,
};
top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120));
top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120));
top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 180));
var lbl = new Label { Name = "ModProjectLabel", AutoSize = true, Anchor = AnchorStyles.Left };
top.Controls.Add(lbl, 0, 0);
_txtModProjectPath.Dock = DockStyle.Fill;
_txtModProjectPath.Margin = new Padding(4);
_txtModProjectPath.Text = Path.Combine(ResolveRepoRoot(), "gregMod.GregifyEmployees");
top.Controls.Add(_txtModProjectPath, 1, 0);
_btnBrowseModProject.Dock = DockStyle.Fill;
_btnBrowseModProject.Click += (_, _) => BrowseFolder(_txtModProjectPath);
top.Controls.Add(_btnBrowseModProject, 2, 0);
_btnOpenModProject.Dock = DockStyle.Fill;
_btnOpenModProject.Click += (_, _) => OpenDirectory(_txtModProjectPath.Text.Trim(), "mod-project");
top.Controls.Add(_btnOpenModProject, 3, 0);
_btnAnalyzeModProject.Dock = DockStyle.Fill;
_btnAnalyzeModProject.Click += async (_, _) => await AnalyzeModProjectAsync();
top.Controls.Add(_btnAnalyzeModProject, 4, 0);
root.Controls.Add(top, 0, 0);
var modTabs = new TabControl
{
Dock = DockStyle.Fill,
Name = "ModAnalysisTabs",
};
root.Controls.Add(modTabs, 0, 1);
var summaryTab = new TabPage { Name = "ModSummaryTab" };
_txtModAnalysisSummary.Dock = DockStyle.Fill;
_txtModAnalysisSummary.Multiline = true;
_txtModAnalysisSummary.ScrollBars = ScrollBars.Both;
_txtModAnalysisSummary.Font = new System.Drawing.Font("Consolas", 10f);
summaryTab.Controls.Add(_txtModAnalysisSummary);
modTabs.TabPages.Add(summaryTab);
var opportunitiesTab = new TabPage { Name = "ModOpportunitiesTab" };
_gridMigration.Dock = DockStyle.Fill;
_gridMigration.AllowUserToAddRows = false;
_gridMigration.AllowUserToDeleteRows = false;
_gridMigration.ReadOnly = true;
_gridMigration.AutoGenerateColumns = false;
_gridMigration.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridMigration.MultiSelect = false;
_gridMigration.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(MigrationOpportunityRow.Type), Name = "MigrationTypeColumn", Width = 150 });
_gridMigration.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(MigrationOpportunityRow.CurrentPattern), Name = "CurrentPatternColumn", Width = 280 });
_gridMigration.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(MigrationOpportunityRow.SuggestedGregHook), Name = "SuggestedHookColumn", Width = 320 });
_gridMigration.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(MigrationOpportunityRow.Suggestion), Name = "SuggestionColumn", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill });
opportunitiesTab.Controls.Add(_gridMigration);
modTabs.TabPages.Add(opportunitiesTab);
var fileInsightsTab = new TabPage { Name = "ModFileInsightsTab" };
_gridFileInsights.Dock = DockStyle.Fill;
_gridFileInsights.AllowUserToAddRows = false;
_gridFileInsights.AllowUserToDeleteRows = false;
_gridFileInsights.ReadOnly = true;
_gridFileInsights.AutoGenerateColumns = false;
_gridFileInsights.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridFileInsights.MultiSelect = false;
_gridFileInsights.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(ModProjectFileInsightRow.FilePath), Name = "FilePathColumn", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill });
_gridFileInsights.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(ModProjectFileInsightRow.HarmonyPatches), Name = "FileHarmonyColumn", Width = 90 });
_gridFileInsights.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(ModProjectFileInsightRow.GregSubscriptions), Name = "FileGregSubsColumn", Width = 90 });
_gridFileInsights.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(ModProjectFileInsightRow.GregApiReferences), Name = "FileGregApiColumn", Width = 90 });
_gridFileInsights.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(ModProjectFileInsightRow.NeedsMigration), Name = "FileNeedsMigrationColumn", Width = 90 });
_gridFileInsights.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(ModProjectFileInsightRow.Recommendation), Name = "FileRecommendationColumn", Width = 360 });
fileInsightsTab.Controls.Add(_gridFileInsights);
modTabs.TabPages.Add(fileInsightsTab);
var hooksTab = new TabPage { Name = "ModUsedHooksTab" };
_txtUsedHooks.Dock = DockStyle.Fill;
_txtUsedHooks.Multiline = true;
_txtUsedHooks.ReadOnly = true;
_txtUsedHooks.ScrollBars = ScrollBars.Both;
_txtUsedHooks.Font = new System.Drawing.Font("Consolas", 10f);
hooksTab.Controls.Add(_txtUsedHooks);
modTabs.TabPages.Add(hooksTab);
return tab;
}
private TabPage BuildLogTab()
{
var tab = new TabPage();
_txtLog.Dock = DockStyle.Fill;
_txtLog.Multiline = true;
_txtLog.ScrollBars = ScrollBars.Both;
_txtLog.Font = new System.Drawing.Font("Consolas", 10f);
tab.Controls.Add(_txtLog);
return tab;
}
private TabPage BuildHelpTab()
{
var tab = new TabPage();
_txtHelp.Dock = DockStyle.Fill;
_txtHelp.Multiline = true;
_txtHelp.ReadOnly = true;
_txtHelp.ScrollBars = ScrollBars.Both;
_txtHelp.Font = new System.Drawing.Font("Consolas", 10f);
tab.Controls.Add(_txtHelp);
return tab;
}
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 = _language == UiLanguage.German ? "Ordner auswählen" : "Select folder",
};
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(_language == UiLanguage.German ? "Watcher deaktiviert." : "Watcher disabled.");
return;
}
string sourceRoot = _txtSourceRoot.Text.Trim();
if (!Directory.Exists(sourceRoot))
{
AppendLog(_language == UiLanguage.German ? "Watcher konnte nicht gestartet werden: SourceRoot nicht gefunden." : "Watcher not started: source root not found.");
_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(_language == UiLanguage.German
? $"Watcher aktiv: {directories.Length} Source-Ordner."
: $"Watcher enabled: {directories.Length} source folders.");
}
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(_language == UiLanguage.German
? $"Scan fertig. Methods: {current.Methods.Count}, Added: {report.Added}, Removed: {report.Removed}, SignatureChanged: {report.SignatureChanged}, BodyChanged: {report.BodyChanged}"
: $"Scan complete. Methods: {current.Methods.Count}, Added: {report.Added}, Removed: {report.Removed}, SignatureChanged: {report.SignatureChanged}, BodyChanged: {report.BodyChanged}");
}, _language == UiLanguage.German ? "Scanne Änderungen..." : "Scanning changes...");
}
private async Task ScanAndSyncIfNeededAsync()
{
await RunExclusiveAsync(async () =>
{
await ScanAndSyncCoreAsync();
}, _language == UiLanguage.German ? "Scanne + synchronisiere..." : "Scanning + syncing...");
}
private async Task ImportFromMelonAsync()
{
await RunExclusiveAsync(async () =>
{
string melonRoot = _txtMelonGeneratedRoot.Text.Trim();
string sourceRoot = _txtSourceRoot.Text.Trim();
MelonImportResult result = _automation.ImportMelonGeneratedSources(melonRoot, sourceRoot);
AppendLog(_language == UiLanguage.German
? $"Melon-Import fertig. Dirs: {result.CopiedDirectories}, Files: {result.CopiedFiles}"
: $"Melon import completed. Dirs: {result.CopiedDirectories}, Files: {result.CopiedFiles}");
AppendLog(_language == UiLanguage.German ? $"Quelle: {result.SourceRootUsed}" : $"Source: {result.SourceRootUsed}");
AppendLog(_language == UiLanguage.German ? $"Ziel: {result.TargetRoot}" : $"Target: {result.TargetRoot}");
await ScanAndSyncCoreAsync();
}, _language == UiLanguage.German ? "Importiere Melon-generated Dateien..." : "Importing Melon generated files...");
}
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(_language == UiLanguage.German
? $"Game-Import fertig. Dirs: {result.CopiedDirectories}, Files: {result.CopiedFiles}"
: $"Game import completed. Dirs: {result.CopiedDirectories}, Files: {result.CopiedFiles}");
AppendLog(_language == UiLanguage.German ? $"Spiel: {gameRoot}" : $"Game: {gameRoot}");
AppendLog(_language == UiLanguage.German ? $"Quelle: {result.SourceRootUsed}" : $"Source: {result.SourceRootUsed}");
AppendLog(_language == UiLanguage.German ? $"Ziel: {result.TargetRoot}" : $"Target: {result.TargetRoot}");
await ScanAndSyncCoreAsync();
}, _language == UiLanguage.German ? "Importiere Daten aus Spielordner..." : "Importing data from game directory...");
}
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,
outputDir,
pluginName,
rootNamespace,
className,
"gregExtractor",
_allHookRows);
_lastGeneratedTemplateDirectory = outputDir;
_btnOpenTemplateOutput.Enabled = Directory.Exists(outputDir);
AppendLog(_language == UiLanguage.German ? "Plugin-Template erzeugt:" : "Plugin template generated:");
AppendLog($"- {csprojPath}");
AppendLog($"- {mainCsPath}");
AppendLog($"- {readmePath}");
await Task.CompletedTask;
}, _language == UiLanguage.German ? "Erzeuge Plugin-Template..." : "Generating plugin template...");
}
private void OpenTemplateOutputFolder()
{
string path = !string.IsNullOrWhiteSpace(_lastGeneratedTemplateDirectory)
? _lastGeneratedTemplateDirectory
: _txtTemplateOutput.Text.Trim();
OpenDirectory(path, "template-output");
}
private void OpenDirectory(string path, string context)
{
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
{
AppendLog(_language == UiLanguage.German
? $"Ordner nicht gefunden ({context}): {path}"
: $"Directory not found ({context}): {path}");
return;
}
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
});
}
private async Task AnalyzeModProjectAsync()
{
await RunExclusiveAsync(async () =>
{
string projectPath = _txtModProjectPath.Text.Trim();
if (string.IsNullOrWhiteSpace(projectPath) || !Directory.Exists(projectPath))
throw new DirectoryNotFoundException(_language == UiLanguage.German ? "Modprojekt-Pfad nicht gefunden." : "Mod project path not found.");
string repoRoot = _txtRepoRoot.Text.Trim();
List<HookCatalogRow> gregCoreRows = _automation.LoadGregCoreImplementedHookRows(repoRoot).ToList();
if (gregCoreRows.Count == 0)
gregCoreRows = _automation.LoadHookCatalogRows(repoRoot, _txtMelonGeneratedRoot.Text.Trim()).ToList();
ModProjectAnalysisResult analysis = _modAnalyzer.Analyze(projectPath, gregCoreRows);
SetModAnalysisSummary(analysis);
_gridMigration.DataSource = analysis.Opportunities;
_gridFileInsights.DataSource = analysis.FileInsights;
var usedHooksBuilder = new StringBuilder();
if (_language == UiLanguage.German)
{
usedHooksBuilder.AppendLine("Im Projekt genutzte greg Hooks:");
foreach (string hook in analysis.UsedHooks)
usedHooksBuilder.AppendLine($"- {hook}");
usedHooksBuilder.AppendLine();
usedHooksBuilder.AppendLine("Empfohlene zusätzliche Hooks:");
foreach (string hook in analysis.SuggestedHooks.Take(30))
usedHooksBuilder.AppendLine($"- {hook}");
}
else
{
usedHooksBuilder.AppendLine("greg hooks used in project:");
foreach (string hook in analysis.UsedHooks)
usedHooksBuilder.AppendLine($"- {hook}");
usedHooksBuilder.AppendLine();
usedHooksBuilder.AppendLine("Recommended additional hooks:");
foreach (string hook in analysis.SuggestedHooks.Take(30))
usedHooksBuilder.AppendLine($"- {hook}");
}
_txtUsedHooks.Text = usedHooksBuilder.ToString();
AppendLog(_language == UiLanguage.German
? $"Modprojekt analysiert: {projectPath}"
: $"Mod project analyzed: {projectPath}");
await Task.CompletedTask;
}, _language == UiLanguage.German ? "Analysiere Modprojekt..." : "Analyzing mod project...");
}
private void SetModAnalysisSummary(ModProjectAnalysisResult analysis)
{
var sb = new StringBuilder();
if (_language == UiLanguage.German)
{
sb.AppendLine("Modprojekt-Analyse");
sb.AppendLine("------------------------------------------------------------");
sb.AppendLine($"Projekt : {analysis.ProjectRoot}");
sb.AppendLine($"C# Dateien : {analysis.CSharpFileCount}");
sb.AppendLine($"Harmony Patches : {analysis.HarmonyPatchCount}");
sb.AppendLine($"MelonMod Vererbungen : {analysis.MelonModInheritanceCount}");
sb.AppendLine($"gregPluginBase Klassen : {analysis.GregPluginInheritanceCount}");
sb.AppendLine($"greg Event Subs : {analysis.GregEventSubscriptionCount}");
sb.AppendLine($"greg API Referenzen : {analysis.GregApiReferenceCount}");
sb.AppendLine();
sb.AppendLine($"Migrationsgrad : {analysis.MigrationPercent:F2}%");
sb.AppendLine($"Integrationspunkte : {analysis.IntegrationPointsMigrated}/{analysis.IntegrationPointsTotal}");
sb.AppendLine($"Noch offen : {analysis.IntegrationPointsRemaining}");
sb.AppendLine();
sb.AppendLine($"Bekannte gregCore Hooks: {analysis.KnownGregCoreHooks}");
sb.AppendLine($"Im Projekt genutzt : {analysis.UsedGregCoreHooks}");
sb.AppendLine($"Noch nicht genutzt : {analysis.MissingGregCoreHooks}");
sb.AppendLine();
sb.AppendLine("Empfohlene nächste Schritte:");
foreach (string hook in analysis.SuggestedHooks.Take(10))
sb.AppendLine($"- Prüfen: {hook}");
}
else
{
sb.AppendLine("Mod Project Analysis");
sb.AppendLine("------------------------------------------------------------");
sb.AppendLine($"Project : {analysis.ProjectRoot}");
sb.AppendLine($"C# files : {analysis.CSharpFileCount}");
sb.AppendLine($"Harmony patches : {analysis.HarmonyPatchCount}");
sb.AppendLine($"MelonMod inheritances : {analysis.MelonModInheritanceCount}");
sb.AppendLine($"gregPluginBase classes : {analysis.GregPluginInheritanceCount}");
sb.AppendLine($"greg event subs : {analysis.GregEventSubscriptionCount}");
sb.AppendLine($"greg API references : {analysis.GregApiReferenceCount}");
sb.AppendLine();
sb.AppendLine($"Migration level : {analysis.MigrationPercent:F2}%");
sb.AppendLine($"Integration points : {analysis.IntegrationPointsMigrated}/{analysis.IntegrationPointsTotal}");
sb.AppendLine($"Remaining : {analysis.IntegrationPointsRemaining}");
sb.AppendLine();
sb.AppendLine($"Known gregCore hooks : {analysis.KnownGregCoreHooks}");
sb.AppendLine($"Used in project : {analysis.UsedGregCoreHooks}");
sb.AppendLine($"Not used yet : {analysis.MissingGregCoreHooks}");
sb.AppendLine();
sb.AppendLine("Recommended next steps:");
foreach (string hook in analysis.SuggestedHooks.Take(10))
sb.AppendLine($"- Evaluate: {hook}");
}
_txtModAnalysisSummary.Text = sb.ToString();
}
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(_language == UiLanguage.German ? "Keine Änderungen erkannt." : "No changes detected.");
return;
}
AppendLog(_language == UiLanguage.German
? $"Änderungen erkannt: +{report.Added} / -{report.Removed} / SigΔ {report.SignatureChanged} / BodyΔ {report.BodyChanged}"
: $"Changes detected: +{report.Added} / -{report.Removed} / SigΔ {report.SignatureChanged} / BodyΔ {report.BodyChanged}");
if (_chkAutoRegenerate.Checked)
{
AppendLog(_language == UiLanguage.German ? "Auto-Regenerate aktiv -> Generator startet." : "Auto-regenerate enabled -> starting generator.");
await RegenerateHooksCoreAsync(_chkBuildAfterRegenerate.Checked);
}
else
{
AppendLog(_language == UiLanguage.German ? "Auto-Regenerate ist aus. Nur Report aktualisiert." : "Auto-regenerate disabled. Report updated only.");
}
}
private async Task RegenerateHooksAsync(bool buildAfter)
{
await RunExclusiveAsync(async () => await RegenerateHooksCoreAsync(buildAfter), _language == UiLanguage.German ? "Regeneriere Hooks..." : "Regenerating 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(_language == UiLanguage.German ? $"Generator fehlgeschlagen (ExitCode={exitCode})." : $"Generator failed (ExitCode={exitCode}).");
return;
}
AppendLog(_language == UiLanguage.German ? "Generator erfolgreich." : "Generator successful.");
RefreshHookRows(_store.TryLoadSnapshot());
if (buildAfter)
await BuildGregCoreCoreAsync();
}
private async Task BuildGregCoreAsync()
{
await RunExclusiveAsync(async () => await BuildGregCoreCoreAsync(), _language == UiLanguage.German ? "Baue gregCore..." : "Building 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);
AppendLog(exitCode == 0
? (_language == UiLanguage.German ? "Build erfolgreich." : "Build successful.")
: (_language == UiLanguage.German ? $"Build fehlgeschlagen (ExitCode={exitCode})." : $"Build failed (ExitCode={exitCode})."));
}
private async Task RunExclusiveAsync(Func<Task> action, string status)
{
if (!await _operationLock.WaitAsync(0))
{
AppendLog(_language == UiLanguage.German ? "Eine Operation läuft bereits." : "Another operation is already running.");
return;
}
try
{
SetBusyUi(true, status);
await action();
}
catch (Exception ex)
{
AppendLog($"{(_language == UiLanguage.German ? "FEHLER" : "ERROR")}: {ex.Message}");
}
finally
{
SetBusyUi(false, _language == UiLanguage.German ? "Bereit" : "Ready");
_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;
_btnOpenTemplateOutput.Enabled = !busy && Directory.Exists(_txtTemplateOutput.Text.Trim());
_btnBrowseModProject.Enabled = !busy;
_btnAnalyzeModProject.Enabled = !busy;
_btnOpenModProject.Enabled = !busy;
_cmbLanguage.Enabled = !busy;
_lblStatus.Text = $"{T("status")}: {status}";
}
private void SetSummary(ChangeReport report, SourceSnapshot snapshot)
{
var sb = new StringBuilder();
if (_language == UiLanguage.German)
{
sb.AppendLine("gregExtractor - Änderungsübersicht");
sb.AppendLine("------------------------------------------------------------");
sb.AppendLine($"Zeit (UTC) : {report.CreatedUtc:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"SourceRoot : {snapshot.SourceRoot}");
sb.AppendLine($"Source Dateien : {snapshot.FileCount}");
sb.AppendLine($"Vorherige Methoden : {report.PreviousCount}");
sb.AppendLine($"Aktuelle Methoden : {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($"Hat Änderungen : {report.HasChanges}");
sb.AppendLine();
sb.AppendLine("Hinweis:");
sb.AppendLine("- Signature Changed: API-/Signaturänderung erkannt.");
sb.AppendLine("- Body Changed: Verhalten geändert, Signatur gleich.");
}
else
{
sb.AppendLine("gregExtractor - Change Summary");
sb.AppendLine("------------------------------------------------------------");
sb.AppendLine($"Time (UTC) : {report.CreatedUtc:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"SourceRoot : {snapshot.SourceRoot}");
sb.AppendLine($"Source files : {snapshot.FileCount}");
sb.AppendLine($"Previous methods : {report.PreviousCount}");
sb.AppendLine($"Current methods : {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("Notes:");
sb.AppendLine("- Signature changed: API/signature changes detected.");
sb.AppendLine("- Body changed: behavior changed, same signature.");
}
_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 = _language == UiLanguage.German ? $"Katalog: n/a | Hooks: {_allHookRows.Count}" : $"Catalog: n/a | Hooks: {_allHookRows.Count}";
_lblGregCoreCoverage.Text = _language == UiLanguage.German
? $"gregCore Umsetzung: n/a | Hooks: {gregCoreRows.Count}"
: $"gregCore implementation: n/a | hooks: {gregCoreRows.Count}";
_lblAssemblyCoverage.Text = "Assemblies: n/a";
_txtCoverageDetails.Text = _language == UiLanguage.German
? "Keine Snapshot-Daten vorhanden. Bitte zuerst importieren und scannen."
: "No snapshot data available. Please import and scan first.";
_gridAssemblyCoverage.DataSource = null;
return;
}
HookCoverage catalogCoverage = _automation.CalculateCoverage(snapshot, _allHookRows);
HookCoverage gregCoreCoverage = _automation.CalculateCoverage(snapshot, gregCoreRows);
bool pluginReady = Math.Abs(gregCoreCoverage.CoveragePercent - 100d) < 0.0001;
List<AssemblyCoverageRow> assemblyRows = _automation.CalculateCoverageByAssembly(snapshot, gregCoreRows).ToList();
if (_language == UiLanguage.German)
{
_lblCoverage.Text = $"Katalog: {catalogCoverage.CoveragePercent:F2}% ({catalogCoverage.CoveredUnique}/{catalogCoverage.ExpectedUnique}) Missing: {catalogCoverage.MissingUnique}";
_lblGregCoreCoverage.Text = $"gregCore Umsetzung: {gregCoreCoverage.CoveragePercent:F2}% ({gregCoreCoverage.CoveredUnique}/{gregCoreCoverage.ExpectedUnique}) Missing: {gregCoreCoverage.MissingUnique} | Plugin-Ready: {(pluginReady ? "JA" : "NEIN")}";
_lblAssemblyCoverage.Text = $"gregCore Assemblies: {assemblyRows.Count}";
}
else
{
_lblCoverage.Text = $"Catalog: {catalogCoverage.CoveragePercent:F2}% ({catalogCoverage.CoveredUnique}/{catalogCoverage.ExpectedUnique}) Missing: {catalogCoverage.MissingUnique}";
_lblGregCoreCoverage.Text = $"gregCore implementation: {gregCoreCoverage.CoveragePercent:F2}% ({gregCoreCoverage.CoveredUnique}/{gregCoreCoverage.ExpectedUnique}) Missing: {gregCoreCoverage.MissingUnique} | Plugin-ready: {(pluginReady ? "YES" : "NO")}";
_lblAssemblyCoverage.Text = $"gregCore assemblies: {assemblyRows.Count}";
}
var details = new StringBuilder();
if (_language == UiLanguage.German)
{
details.AppendLine("Coverage-Übersicht");
details.AppendLine("------------------------------------------------------------");
details.AppendLine($"Snapshot Methoden : {snapshot.Methods.Count}");
details.AppendLine($"Katalog-Abdeckung : {catalogCoverage.CoveragePercent:F2}%");
details.AppendLine($"gregCore-Umsetzung : {gregCoreCoverage.CoveragePercent:F2}%");
details.AppendLine($"Plugin-Ready : {(pluginReady ? "JA" : "NEIN")}");
details.AppendLine($"Fehlende gregCore Signaturen: {gregCoreCoverage.MissingUnique}");
details.AppendLine();
details.AppendLine("Top Assemblies mit Lücken:");
foreach (AssemblyCoverageRow row in assemblyRows.OrderByDescending(x => x.MissingUnique).Take(12))
details.AppendLine($"- {row.Assembly}: Missing {row.MissingUnique}, Coverage {row.CoveragePercent:F2}%");
}
else
{
details.AppendLine("Coverage Overview");
details.AppendLine("------------------------------------------------------------");
details.AppendLine($"Snapshot methods : {snapshot.Methods.Count}");
details.AppendLine($"Catalog coverage : {catalogCoverage.CoveragePercent:F2}%");
details.AppendLine($"gregCore implementation : {gregCoreCoverage.CoveragePercent:F2}%");
details.AppendLine($"Plugin-ready : {(pluginReady ? "YES" : "NO")}");
details.AppendLine($"Missing gregCore signatures : {gregCoreCoverage.MissingUnique}");
details.AppendLine();
details.AppendLine("Top assemblies with gaps:");
foreach (AssemblyCoverageRow row in assemblyRows.OrderByDescending(x => x.MissingUnique).Take(12))
details.AppendLine($"- {row.Assembly}: Missing {row.MissingUnique}, Coverage {row.CoveragePercent:F2}%");
}
_txtCoverageDetails.Text = details.ToString();
_gridAssemblyCoverage.DataSource = assemblyRows;
}
catch (Exception ex)
{
_lblCoverage.Text = _language == UiLanguage.German ? "Katalog: Fehler" : "Catalog: error";
_lblGregCoreCoverage.Text = _language == UiLanguage.German ? "gregCore Umsetzung: Fehler" : "gregCore implementation: error";
_lblAssemblyCoverage.Text = _language == UiLanguage.German ? "Assemblies: Fehler" : "Assemblies: error";
_txtCoverageDetails.Text = ex.Message;
_gridAssemblyCoverage.DataSource = null;
AppendLog(ex.Message);
}
}
private void UpdateCategoryOptions()
{
string allLabel = T("all");
string previous = _cmbCategory.SelectedItem?.ToString() ?? allLabel;
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(allLabel);
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() ?? T("all");
bool allSelected = string.Equals(selected, T("all"), StringComparison.OrdinalIgnoreCase)
|| string.Equals(selected, "Alle", StringComparison.OrdinalIgnoreCase)
|| string.Equals(selected, "All", StringComparison.OrdinalIgnoreCase);
List<HookCatalogRow> filtered = allSelected
? _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 ApplyLanguage()
{
_lblTitle.Text = "gregExtractor - Hook Sync & Migration";
_lblLanguage.Text = _language == UiLanguage.German ? "Sprache:" : "Language:";
TabControl? tabControl = Controls.OfType<TabControl>().FirstOrDefault();
if (tabControl != null && tabControl.TabPages.Count >= 5)
{
tabControl.TabPages[0].Text = T("tab.workflow");
tabControl.TabPages[1].Text = T("tab.coverage");
tabControl.TabPages[2].Text = T("tab.modanalysis");
tabControl.TabPages[3].Text = T("tab.log");
tabControl.TabPages[4].Text = T("tab.help");
}
TabControl? coverageTabs = GetAllControls(this).OfType<TabControl>().FirstOrDefault(x => x.Name == "CoverageTabs");
if (coverageTabs != null && coverageTabs.TabPages.Count >= 3)
{
coverageTabs.TabPages[0].Text = T("tab.coverage.overview");
coverageTabs.TabPages[1].Text = T("tab.coverage.hooks");
coverageTabs.TabPages[2].Text = T("tab.coverage.assemblies");
}
TabControl? modTabs = GetAllControls(this).OfType<TabControl>().FirstOrDefault(x => x.Name == "ModAnalysisTabs");
if (modTabs != null && modTabs.TabPages.Count >= 4)
{
modTabs.TabPages[0].Text = T("tab.mod.summary");
modTabs.TabPages[1].Text = T("tab.mod.opportunities");
modTabs.TabPages[2].Text = T("tab.mod.fileinsights");
modTabs.TabPages[3].Text = T("tab.mod.hooks");
}
foreach (Control control in GetAllControls(this))
{
switch (control)
{
case GroupBox group when control.Name == "WorkflowPathsGroup":
group.Text = T("workflow.paths.group");
break;
case GroupBox group when control.Name == "WorkflowSummaryGroup":
group.Text = T("workflow.summary.group");
break;
case Label label when label.Name == "WorkflowHintLabel":
label.Text = T("workflow.hint");
break;
case Label label when label.Name == "CategoryLabel":
label.Text = T("category");
break;
case Label label when label.Name == "ModProjectLabel":
label.Text = T("mod.project.path");
break;
case Label label when label.Name == "PathRepoLabel":
label.Text = T("path.repo");
break;
case Label label when label.Name == "PathSourceLabel":
label.Text = T("path.source");
break;
case Label label when label.Name == "PathMelonLabel":
label.Text = T("path.melon");
break;
case Label label when label.Name == "PathGameLabel":
label.Text = T("path.game");
break;
case Label label when label.Name == "PathTemplateLabel":
label.Text = T("path.template");
break;
case Label label when label.Name == "PathPluginLabel":
label.Text = T("path.plugin");
break;
case Button button when button.Name == "RefreshCoverageButton":
button.Text = T("btn.refresh");
break;
case Button button when button.Name.EndsWith("_Browse", StringComparison.Ordinal):
button.Text = T("btn.browse");
break;
}
}
_btnImportMelon.Text = T("btn.import.melon");
_btnImportGame.Text = T("btn.import.game");
_btnGenerateTemplate.Text = T("btn.template.generate");
_btnOpenTemplateOutput.Text = T("btn.open");
_btnScan.Text = T("btn.scan");
_btnSync.Text = T("btn.sync");
_btnRegenerate.Text = T("btn.regenerate");
_btnBuild.Text = T("btn.build");
_btnBrowseModProject.Text = T("btn.browse");
_btnOpenModProject.Text = T("btn.open");
_btnAnalyzeModProject.Text = T("btn.mod.analyze");
_chkWatch.Text = T("chk.watch");
_chkAutoRegenerate.Text = T("chk.autoregen");
_chkBuildAfterRegenerate.Text = T("chk.buildafter");
_lblStatus.Text = $"{T("status")}: {T("ready")}";
_txtHelp.Text = BuildHelpText();
UpdateGridHeaders();
RefreshHookRows(_store.TryLoadSnapshot());
}
private void UpdateGridHeaders()
{
_gridHooks.Columns[0].HeaderText = T("col.hookevent");
_gridHooks.Columns[1].HeaderText = T("col.gregapi");
_gridHooks.Columns[2].HeaderText = T("col.category");
_gridHooks.Columns[3].HeaderText = T("col.patchtarget");
_gridAssemblyCoverage.Columns[0].HeaderText = T("col.assembly");
_gridAssemblyCoverage.Columns[1].HeaderText = T("col.coveragepercent");
_gridAssemblyCoverage.Columns[2].HeaderText = T("col.covered");
_gridAssemblyCoverage.Columns[3].HeaderText = T("col.expected");
_gridAssemblyCoverage.Columns[4].HeaderText = T("col.missing");
_gridAssemblyCoverage.Columns[5].HeaderText = T("col.hooked");
_gridMigration.Columns[0].HeaderText = T("col.type");
_gridMigration.Columns[1].HeaderText = T("col.currentpattern");
_gridMigration.Columns[2].HeaderText = T("col.suggestedhook");
_gridMigration.Columns[3].HeaderText = T("col.suggestion");
_gridFileInsights.Columns[0].HeaderText = T("col.filepath");
_gridFileInsights.Columns[1].HeaderText = T("col.fileharmony");
_gridFileInsights.Columns[2].HeaderText = T("col.filesubs");
_gridFileInsights.Columns[3].HeaderText = T("col.fileapi");
_gridFileInsights.Columns[4].HeaderText = T("col.needsmigration");
_gridFileInsights.Columns[5].HeaderText = T("col.recommendation");
}
private string BuildHelpText()
{
var sb = new StringBuilder();
if (_language == UiLanguage.German)
{
sb.AppendLine("gregExtractor - Help");
sb.AppendLine("============================================================");
sb.AppendLine();
sb.AppendLine("Was ist 'Melon Generated'?");
sb.AppendLine("- Das ist der Ordner mit generierten C#-Quellen von Melon/Il2Cpp.");
sb.AppendLine("- Er kann im Spieleordner liegen (z. B. <Game>\\MelonLoader\\Generated)");
sb.AppendLine(" oder unter %LocalAppData%\\MelonLoader.");
sb.AppendLine("- Es ist NICHT nur ein beliebiger Installationspfad, sondern der Output mit Source-Dateien.");
sb.AppendLine();
sb.AppendLine("Tabs:");
sb.AppendLine("1) Workflow:");
sb.AppendLine(" - Import aus Melon Generated oder direkt aus Game-Ordner.");
sb.AppendLine(" - Scan/Sync/Regenerate/Build.");
sb.AppendLine(" - Template erzeugen und direkt per 'Öffnen' den Zielordner öffnen.");
sb.AppendLine("2) Hook Coverage:");
sb.AppendLine(" - Katalog-Abdeckung vs. Snapshot.");
sb.AppendLine(" - gregCore-Umsetzung + Plugin-Ready (JA/NEIN).");
sb.AppendLine(" - Assembly-Tabelle für Lückenanalyse.");
sb.AppendLine("3) Modprojekt Analyse:");
sb.AppendLine(" - Beliebigen Modprojekt-Ordner wählen.");
sb.AppendLine(" - Prüfen, wie weit das Projekt auf gregCore migriert ist.");
sb.AppendLine(" - Konkrete Umsetzungsansätze in der Opportunity-Tabelle erhalten.");
sb.AppendLine("4) Log: Alle Aktionen und Outputs.");
sb.AppendLine("5) Help: Diese Referenzseite.");
}
else
{
sb.AppendLine("gregExtractor - Help");
sb.AppendLine("============================================================");
sb.AppendLine();
sb.AppendLine("What is 'Melon Generated'?");
sb.AppendLine("- It is the folder containing generated C# sources from Melon/Il2Cpp.");
sb.AppendLine("- It can be inside the game folder (e.g. <Game>\\MelonLoader\\Generated)");
sb.AppendLine(" or in %LocalAppData%\\MelonLoader.");
sb.AppendLine("- It is not just any installation path; it is the source-output location.");
sb.AppendLine();
sb.AppendLine("Tabs:");
sb.AppendLine("1) Workflow:");
sb.AppendLine(" - Import from Melon Generated or directly from game directory.");
sb.AppendLine(" - Run scan/sync/regenerate/build.");
sb.AppendLine(" - Generate template and open target folder with 'Open'.");
sb.AppendLine("2) Hook Coverage:");
sb.AppendLine(" - Catalog coverage vs snapshot.");
sb.AppendLine(" - gregCore implementation + Plugin-ready (YES/NO).");
sb.AppendLine(" - Assembly table for gap analysis.");
sb.AppendLine("3) Mod Project Analysis:");
sb.AppendLine(" - Pick any mod project directory.");
sb.AppendLine(" - Check how much is migrated to gregCore usage.");
sb.AppendLine(" - Get concrete migration opportunities and implementation hints.");
sb.AppendLine("4) Log: all actions and command outputs.");
sb.AppendLine("5) Help: this reference page.");
}
return sb.ToString();
}
private static IEnumerable<Control> GetAllControls(Control parent)
{
foreach (Control child in parent.Controls)
{
yield return child;
foreach (Control nested in GetAllControls(child))
yield return nested;
}
}
private string T(string key)
{
return (_language, key) switch
{
(UiLanguage.German, "tab.workflow") => "Workflow",
(UiLanguage.English, "tab.workflow") => "Workflow",
(UiLanguage.German, "tab.coverage") => "Hook Coverage",
(UiLanguage.English, "tab.coverage") => "Hook Coverage",
(UiLanguage.German, "tab.coverage.overview") => "Übersicht",
(UiLanguage.English, "tab.coverage.overview") => "Overview",
(UiLanguage.German, "tab.coverage.hooks") => "Hook Tabelle",
(UiLanguage.English, "tab.coverage.hooks") => "Hook Table",
(UiLanguage.German, "tab.coverage.assemblies") => "Assembly Tabelle",
(UiLanguage.English, "tab.coverage.assemblies") => "Assembly Table",
(UiLanguage.German, "tab.modanalysis") => "Modprojekt Analyse",
(UiLanguage.English, "tab.modanalysis") => "Mod Project Analysis",
(UiLanguage.German, "tab.mod.summary") => "Zusammenfassung",
(UiLanguage.English, "tab.mod.summary") => "Summary",
(UiLanguage.German, "tab.mod.opportunities") => "Opportunities",
(UiLanguage.English, "tab.mod.opportunities") => "Opportunities",
(UiLanguage.German, "tab.mod.fileinsights") => "Datei-Insights",
(UiLanguage.English, "tab.mod.fileinsights") => "File Insights",
(UiLanguage.German, "tab.mod.hooks") => "Hook Nutzung",
(UiLanguage.English, "tab.mod.hooks") => "Hook Usage",
(UiLanguage.German, "tab.log") => "Log",
(UiLanguage.English, "tab.log") => "Log",
(UiLanguage.German, "tab.help") => "Help",
(UiLanguage.English, "tab.help") => "Help",
(UiLanguage.German, "workflow.paths.group") => "Import, Pfade und Aktionen",
(UiLanguage.English, "workflow.paths.group") => "Import, Paths and Actions",
(UiLanguage.German, "workflow.summary.group") => "Änderungsübersicht",
(UiLanguage.English, "workflow.summary.group") => "Change Summary",
(UiLanguage.German, "path.repo") => "Repo:",
(UiLanguage.English, "path.repo") => "Repo:",
(UiLanguage.German, "path.source") => "Source:",
(UiLanguage.English, "path.source") => "Source:",
(UiLanguage.German, "path.melon") => "Melon Generated:",
(UiLanguage.English, "path.melon") => "Melon Generated:",
(UiLanguage.German, "path.game") => "Game:",
(UiLanguage.English, "path.game") => "Game:",
(UiLanguage.German, "path.template") => "Template Ziel:",
(UiLanguage.English, "path.template") => "Template Output:",
(UiLanguage.German, "path.plugin") => "Plugin Name:",
(UiLanguage.English, "path.plugin") => "Plugin Name:",
(UiLanguage.German, "btn.import.melon") => "Melon importieren",
(UiLanguage.English, "btn.import.melon") => "Import Melon",
(UiLanguage.German, "btn.import.game") => "Aus Spielordner importieren",
(UiLanguage.English, "btn.import.game") => "Import From Game",
(UiLanguage.German, "btn.template.generate") => "Plugin Template bauen (alle Hooks)",
(UiLanguage.English, "btn.template.generate") => "Generate Plugin Template (all hooks)",
(UiLanguage.German, "btn.open") => "Öffnen",
(UiLanguage.English, "btn.open") => "Open",
(UiLanguage.German, "btn.scan") => "1) Nur Änderungen scannen",
(UiLanguage.English, "btn.scan") => "1) Scan changes only",
(UiLanguage.German, "btn.sync") => "2) Scan + bei Änderung syncen",
(UiLanguage.English, "btn.sync") => "2) Scan + sync if changed",
(UiLanguage.German, "btn.regenerate") => "Hooks regenerieren",
(UiLanguage.English, "btn.regenerate") => "Regenerate hooks",
(UiLanguage.German, "btn.build") => "gregCore builden",
(UiLanguage.English, "btn.build") => "Build gregCore",
(UiLanguage.German, "btn.browse") => "Wählen",
(UiLanguage.English, "btn.browse") => "Browse",
(UiLanguage.German, "btn.mod.analyze") => "Modprojekt analysieren",
(UiLanguage.English, "btn.mod.analyze") => "Analyze mod project",
(UiLanguage.German, "btn.refresh") => "Aktualisieren",
(UiLanguage.English, "btn.refresh") => "Refresh",
(UiLanguage.German, "chk.watch") => "Dateien überwachen (continuous mode)",
(UiLanguage.English, "chk.watch") => "Watch files (continuous mode)",
(UiLanguage.German, "chk.autoregen") => "Auto-Regenerate bei Änderungen",
(UiLanguage.English, "chk.autoregen") => "Auto regenerate on changes",
(UiLanguage.German, "chk.buildafter") => "Nach Regeneration automatisch builden",
(UiLanguage.English, "chk.buildafter") => "Build automatically after regeneration",
(UiLanguage.German, "status") => "Status",
(UiLanguage.English, "status") => "Status",
(UiLanguage.German, "ready") => "Bereit",
(UiLanguage.English, "ready") => "Ready",
(UiLanguage.German, "all") => "Alle",
(UiLanguage.English, "all") => "All",
(UiLanguage.German, "workflow.hint") => "Tipp: Import -> Scan/Sync -> Regenerate -> Build -> Coverage/Analyse prüfen.",
(UiLanguage.English, "workflow.hint") => "Tip: Import -> Scan/Sync -> Regenerate -> Build -> Review coverage/analysis.",
(UiLanguage.German, "category") => "Kategorie:",
(UiLanguage.English, "category") => "Category:",
(UiLanguage.German, "mod.project.path") => "Modprojekt:",
(UiLanguage.English, "mod.project.path") => "Mod project:",
(UiLanguage.German, "mod.summary.group") => "Migrationsstatus & Zusammenfassung",
(UiLanguage.English, "mod.summary.group") => "Migration Status & Summary",
(UiLanguage.German, "mod.opportunities.group") => "Migration Opportunities (wie umsetzen)",
(UiLanguage.English, "mod.opportunities.group") => "Migration Opportunities (how to implement)",
(UiLanguage.German, "col.hookevent") => "IL2CPP Hook Event",
(UiLanguage.English, "col.hookevent") => "IL2CPP Hook Event",
(UiLanguage.German, "col.gregapi") => "greg API Call",
(UiLanguage.English, "col.gregapi") => "greg API Call",
(UiLanguage.German, "col.category") => "Kategorie",
(UiLanguage.English, "col.category") => "Category",
(UiLanguage.German, "col.patchtarget") => "Patch Target",
(UiLanguage.English, "col.patchtarget") => "Patch Target",
(UiLanguage.German, "col.assembly") => "Assembly",
(UiLanguage.English, "col.assembly") => "Assembly",
(UiLanguage.German, "col.coveragepercent") => "Coverage %",
(UiLanguage.English, "col.coveragepercent") => "Coverage %",
(UiLanguage.German, "col.covered") => "Covered",
(UiLanguage.English, "col.covered") => "Covered",
(UiLanguage.German, "col.expected") => "Expected",
(UiLanguage.English, "col.expected") => "Expected",
(UiLanguage.German, "col.missing") => "Missing",
(UiLanguage.English, "col.missing") => "Missing",
(UiLanguage.German, "col.hooked") => "Hooked",
(UiLanguage.English, "col.hooked") => "Hooked",
(UiLanguage.German, "col.type") => "Typ",
(UiLanguage.English, "col.type") => "Type",
(UiLanguage.German, "col.currentpattern") => "Aktuelles Muster",
(UiLanguage.English, "col.currentpattern") => "Current Pattern",
(UiLanguage.German, "col.suggestedhook") => "Vorgeschlagener greg Hook",
(UiLanguage.English, "col.suggestedhook") => "Suggested greg Hook",
(UiLanguage.German, "col.suggestion") => "Umsetzungsvorschlag",
(UiLanguage.English, "col.suggestion") => "Implementation Suggestion",
(UiLanguage.German, "col.filepath") => "Datei",
(UiLanguage.English, "col.filepath") => "File",
(UiLanguage.German, "col.fileharmony") => "Harmony",
(UiLanguage.English, "col.fileharmony") => "Harmony",
(UiLanguage.German, "col.filesubs") => "greg Subs",
(UiLanguage.English, "col.filesubs") => "greg Subs",
(UiLanguage.German, "col.fileapi") => "greg API",
(UiLanguage.English, "col.fileapi") => "greg API",
(UiLanguage.German, "col.needsmigration") => "Migration nötig",
(UiLanguage.English, "col.needsmigration") => "Needs migration",
(UiLanguage.German, "col.recommendation") => "Empfehlung",
(UiLanguage.English, "col.recommendation") => "Recommendation",
_ => key,
};
}
}