Refactor ViewModels to inherit from BaseViewModel and improve UI thread handling
- Updated HookBrowserViewModel, MainViewModel, SettingsViewModel, and SyncViewModel to inherit from BaseViewModel. - Introduced BaseViewModel with methods for running actions on the UI thread. - Enhanced SyncViewModel to handle property changes on the UI thread. - Refactored ExtractorService to return ExtractorResult instead of IReadOnlyList<HookDefinition>. - Added ExtractorResult class to encapsulate extraction results, including success status and error messages. - Implemented UIProgress class for reporting progress on the UI thread. - Updated hook_groups.json with additional classes and methods for various categories. - Improved error handling and logging in ExtractorService and SyncViewModel.
This commit is contained in:
@@ -17,11 +17,10 @@ public sealed class ExtractCommand : AsyncCommand<ExtractCommand.Settings>
|
||||
AnsiConsole.Write(new FigletText("gregExtractor").Color(Color.Cyan1));
|
||||
|
||||
string workingDirectory = Directory.GetCurrentDirectory();
|
||||
string groupsPath = Path.Combine(workingDirectory, "hook_groups.json");
|
||||
|
||||
var logMessages = new List<string>();
|
||||
var progress = new Progress<string>(message => logMessages.Add(message));
|
||||
HookClassifier classifier = HookClassifier.LoadFromFile(groupsPath, progress);
|
||||
HookClassifier classifier = HookClassifier.LoadFromFile(null, progress);
|
||||
|
||||
string? il2CppPath = settings.Path;
|
||||
if (string.IsNullOrWhiteSpace(il2CppPath))
|
||||
@@ -39,17 +38,25 @@ public sealed class ExtractCommand : AsyncCommand<ExtractCommand.Settings>
|
||||
return -1;
|
||||
}
|
||||
|
||||
IReadOnlyList<HookDefinition> hooks = Array.Empty<HookDefinition>();
|
||||
var extractor = new ExtractorService();
|
||||
ExtractorResult result = ExtractorResult.Failure("Not executed");
|
||||
|
||||
await AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.Dots)
|
||||
.StartAsync("Extracting IL2CPP hooks...", async _ =>
|
||||
{
|
||||
hooks = await extractor.ExtractAsync(il2CppPath!, classifier, progress, CancellationToken.None).ConfigureAwait(false);
|
||||
result = await extractor.ExtractAsync(il2CppPath!, classifier, progress, CancellationToken.None).ConfigureAwait(false);
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗ {result.Error}[/]");
|
||||
return -1;
|
||||
}
|
||||
|
||||
var hooks = result.Hooks;
|
||||
|
||||
string gameHooksPath = Path.Combine(workingDirectory, "game_hooks.json");
|
||||
await File.WriteAllTextAsync(
|
||||
gameHooksPath,
|
||||
|
||||
+12
-13
@@ -80,17 +80,16 @@ public sealed class SyncCommand : AsyncCommand<SyncCommand.Settings>
|
||||
var logLines = new List<string>();
|
||||
var progress = new Progress<string>(line => logLines.Add(line));
|
||||
|
||||
IReadOnlyList<HookDefinition> newHooks = Array.Empty<HookDefinition>();
|
||||
HookDiff diff = new(Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>());
|
||||
var result = await extractor.ExtractAsync(il2CppPath, classifier, progress, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
await AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.Dots)
|
||||
.StartAsync("Berechne Assembly-Diff...", async _ =>
|
||||
{
|
||||
newHooks = await extractor.ExtractAsync(il2CppPath, classifier, progress, CancellationToken.None).ConfigureAwait(false);
|
||||
diff = diffService.Diff(oldHooks.ToList(), newHooks.ToList());
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗ {result.Error}[/]");
|
||||
return -1;
|
||||
}
|
||||
|
||||
var newHooks = result.Hooks;
|
||||
HookDiff diff = diffService.Diff(oldHooks.ToList(), newHooks.ToList());
|
||||
|
||||
var summary = new Table().RoundedBorder();
|
||||
summary.Title = new TableTitle("Assembly Diff");
|
||||
@@ -101,7 +100,7 @@ public sealed class SyncCommand : AsyncCommand<SyncCommand.Settings>
|
||||
summary.AddRow("- Methoden entfernt", diff.Removed.Count.ToString());
|
||||
AnsiConsole.Write(summary);
|
||||
|
||||
SyncResult result = syncService.Sync(new SyncOptions(
|
||||
SyncResult syncResult = syncService.Sync(new SyncOptions(
|
||||
FrameworkSourceDir: settings.Source,
|
||||
Diff: diff,
|
||||
AllHooks: newHooks.ToList(),
|
||||
@@ -111,10 +110,10 @@ public sealed class SyncCommand : AsyncCommand<SyncCommand.Settings>
|
||||
foreach (string line in logLines)
|
||||
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(line)}[/]");
|
||||
|
||||
foreach (string file in result.FilesWritten)
|
||||
foreach (string file in syncResult.FilesWritten)
|
||||
AnsiConsole.MarkupLine($"[green]✓[/] {Markup.Escape(Path.GetFileName(file))}");
|
||||
|
||||
foreach (string warning in result.Warnings)
|
||||
foreach (string warning in syncResult.Warnings)
|
||||
AnsiConsole.MarkupLine($"[yellow]{Markup.Escape(warning)}[/]");
|
||||
|
||||
if (!settings.DryRun)
|
||||
|
||||
+61
-1
@@ -1,6 +1,8 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using gregExtractor.GUI.ViewModels;
|
||||
|
||||
namespace gregExtractor.GUI;
|
||||
@@ -15,6 +17,9 @@ public partial class App : Application
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow
|
||||
@@ -25,4 +30,59 @@ public partial class App : Application
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
var ex = e.ExceptionObject as Exception;
|
||||
var msg = ex?.ToString() ?? e.ExceptionObject?.ToString() ?? "Unknown";
|
||||
|
||||
var crashLog = Path.Combine(AppContext.BaseDirectory, "crash.log");
|
||||
File.WriteAllText(crashLog, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] CRASH\n{msg}");
|
||||
|
||||
try
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var window = new Window
|
||||
{
|
||||
Title = "gregExtractor - Crash",
|
||||
Width = 500,
|
||||
Height = 200,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
};
|
||||
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = $"Unhandled Exception:\n\n{ex?.Message}\n\nDetails in: {crashLog}",
|
||||
Margin = new Avalonia.Thickness(20),
|
||||
TextWrapping = Avalonia.Media.TextWrapping.Wrap
|
||||
};
|
||||
|
||||
var button = new Button
|
||||
{
|
||||
Content = "OK",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
Margin = new Avalonia.Thickness(20)
|
||||
};
|
||||
button.Click += (_, _) => window.Close();
|
||||
|
||||
var panel = new StackPanel();
|
||||
panel.Children.Add(textBlock);
|
||||
panel.Children.Add(button);
|
||||
window.Content = panel;
|
||||
|
||||
window.Show();
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
e.SetObserved();
|
||||
|
||||
var crashLog = Path.Combine(AppContext.BaseDirectory, "crash.log");
|
||||
File.AppendAllText(crashLog,
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] TASK EXCEPTION\n{e.Exception}\n\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
public abstract class BaseViewModel : ObservableObject
|
||||
{
|
||||
protected void RunOnUIThread(Action action)
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
action();
|
||||
else
|
||||
Dispatcher.UIThread.Post(action);
|
||||
}
|
||||
|
||||
protected async Task RunOnUIThreadAsync(Action action)
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
action();
|
||||
else
|
||||
await Dispatcher.UIThread.InvokeAsync(action);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using gregExtractor.Models;
|
||||
@@ -6,7 +7,7 @@ using gregExtractor.Utils;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
public sealed class CoverageViewModel : ObservableObject
|
||||
public sealed class CoverageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly CoverageAnalyzerService _analyzerService = new();
|
||||
|
||||
@@ -24,12 +25,16 @@ public sealed class CoverageViewModel : ObservableObject
|
||||
|
||||
public CoverageViewModel()
|
||||
{
|
||||
Log($"[Coverage] Initial FrameworkSourceDirsText: '{_frameworkSourceDirsText}'");
|
||||
|
||||
AutoDetectPathCommand = new RelayCommand(AutoDetectPath);
|
||||
BrowseFolderCommand = new AsyncRelayCommand<string?>(BrowseFolderAsync);
|
||||
AnalyzeCoverageCommand = new AsyncRelayCommand(AnalyzeAsync, () => !IsAnalyzing);
|
||||
OpenMarkdownReportCommand = new RelayCommand(OpenMarkdownReport, () => File.Exists(CoverageAnalyzerService.GetReportPaths(OutputBasePath).MarkdownPath));
|
||||
|
||||
AutoDetectPath();
|
||||
|
||||
Log($"[Coverage] After AutoDetect: Il2CppPath='{Il2CppPath}'");
|
||||
}
|
||||
|
||||
public Func<string?, Task<string?>>? BrowseFolderHandler { get; set; }
|
||||
@@ -37,19 +42,19 @@ public sealed class CoverageViewModel : ObservableObject
|
||||
public string Il2CppPath
|
||||
{
|
||||
get => _il2CppPath;
|
||||
set => SetProperty(ref _il2CppPath, value);
|
||||
set => SetProperty(ref _il2CppPath, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string GameHooksPath
|
||||
{
|
||||
get => _gameHooksPath;
|
||||
set => SetProperty(ref _gameHooksPath, value);
|
||||
set => SetProperty(ref _gameHooksPath, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FrameworkSourceDirsText
|
||||
{
|
||||
get => _frameworkSourceDirsText;
|
||||
set => SetProperty(ref _frameworkSourceDirsText, value);
|
||||
set => SetProperty(ref _frameworkSourceDirsText, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string OutputBasePath
|
||||
@@ -67,8 +72,16 @@ public sealed class CoverageViewModel : ObservableObject
|
||||
get => _isAnalyzing;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isAnalyzing, value))
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
if (!SetProperty(ref _isAnalyzing, value))
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => SetProperty(ref _isAnalyzing, value));
|
||||
return;
|
||||
}
|
||||
|
||||
AnalyzeCoverageCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
@@ -127,10 +140,13 @@ public sealed class CoverageViewModel : ObservableObject
|
||||
|
||||
private void AutoDetectPath()
|
||||
{
|
||||
Il2CppPath = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty;
|
||||
string detected = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty;
|
||||
Log($"[SteamLocator] Found: '{detected}'");
|
||||
|
||||
Il2CppPath = detected;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Il2CppPath))
|
||||
Log("Auto-detect for IL2CPP path failed.");
|
||||
Log("Auto-detect: No IL2CPP path found.");
|
||||
else
|
||||
Log($"Auto-detected IL2CPP path: {Il2CppPath}");
|
||||
}
|
||||
@@ -160,61 +176,95 @@ public sealed class CoverageViewModel : ObservableObject
|
||||
|
||||
private async Task AnalyzeAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Il2CppPath) || !Directory.Exists(Il2CppPath))
|
||||
Log($"[Coverage] Input IL2CPP path: '{Il2CppPath}'");
|
||||
|
||||
string trimmedPath = Il2CppPath?.Trim() ?? string.Empty;
|
||||
Log($"[Coverage] Trimmed path: '{trimmedPath}'");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trimmedPath))
|
||||
{
|
||||
Log("IL2CPP path is invalid.");
|
||||
Log("[Coverage] IL2CPP path is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool pathExists = Directory.Exists(trimmedPath);
|
||||
Log($"[Coverage] Path exists: {pathExists}");
|
||||
|
||||
if (!pathExists)
|
||||
{
|
||||
Log($"[Coverage] IL2CPP directory not found.");
|
||||
Log($"[Coverage] Checked path: {trimmedPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool dllExists = File.Exists(Path.Combine(trimmedPath, "Assembly-CSharp.dll"));
|
||||
Log($"[Coverage] Assembly-CSharp.dll exists: {dllExists}");
|
||||
|
||||
if (!dllExists)
|
||||
{
|
||||
Log($"[Coverage] Assembly-CSharp.dll not found in: {trimmedPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(GameHooksPath) || !File.Exists(GameHooksPath))
|
||||
{
|
||||
Log("game_hooks.json path is invalid.");
|
||||
Log($"game_hooks.json path invalid: '{GameHooksPath}'");
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"[Coverage] Framework source dirs text: '{FrameworkSourceDirsText}'");
|
||||
|
||||
string[] sourceDirs = CoveragePathResolver.ResolveSourceDirs(FrameworkSourceDirsText, Directory.GetCurrentDirectory());
|
||||
Log($"[Coverage] Resolved {sourceDirs.Length} source dirs: {string.Join(", ", sourceDirs)}");
|
||||
|
||||
if (sourceDirs.Length == 0)
|
||||
{
|
||||
Log("No valid source directories found. Provide ';' separated paths.");
|
||||
Log("ERROR: No valid source directories found.");
|
||||
Log(" - Add paths separated by ';' in the Framework Source field");
|
||||
Log(" - Or ensure default paths exist: Hooks/, framework/, ../gregCore/Hooks/");
|
||||
return;
|
||||
}
|
||||
|
||||
IsAnalyzing = true;
|
||||
Progress = 0;
|
||||
Entries.Clear();
|
||||
|
||||
RunOnUIThread(() => Entries.Clear());
|
||||
|
||||
try
|
||||
{
|
||||
var progress = new Progress<string>(message =>
|
||||
var progress = new UIProgress<string>(msg =>
|
||||
{
|
||||
Log(message);
|
||||
Log(msg);
|
||||
Progress = Math.Min(99, Progress + 1);
|
||||
});
|
||||
|
||||
CoverageReport report = await Task.Run(() => _analyzerService.Analyze(new AnalyzeOptions(
|
||||
Il2CppDir: Il2CppPath,
|
||||
var report = await Task.Run(() => _analyzerService.Analyze(new AnalyzeOptions(
|
||||
Il2CppDir: trimmedPath,
|
||||
GameHooksJsonPath: GameHooksPath,
|
||||
FrameworkSourceDirs: sourceDirs,
|
||||
OutputBasePath: OutputBasePath,
|
||||
Progress: progress)))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TotalHooks = report.TotalHooks;
|
||||
CoveredCount = report.CoveredCount;
|
||||
PlannedCount = report.PlannedCount;
|
||||
UncoveredCount = report.UncoveredCount;
|
||||
CoveragePercent = report.CoveragePercent;
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
TotalHooks = report.TotalHooks;
|
||||
CoveredCount = report.CoveredCount;
|
||||
PlannedCount = report.PlannedCount;
|
||||
UncoveredCount = report.UncoveredCount;
|
||||
CoveragePercent = report.CoveragePercent;
|
||||
|
||||
foreach (HookCoverageEntry entry in report.Entries)
|
||||
Entries.Add(entry);
|
||||
foreach (HookCoverageEntry entry in report.Entries)
|
||||
Entries.Add(entry);
|
||||
|
||||
Progress = 100;
|
||||
Log($"Coverage analysis done. Coverage: {report.CoveragePercent:F2}%");
|
||||
OpenMarkdownReportCommand.NotifyCanExecuteChanged();
|
||||
Progress = 100;
|
||||
Log($"Coverage analysis done. Coverage: {report.CoveragePercent:F2}%");
|
||||
OpenMarkdownReportCommand.NotifyCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Coverage analysis failed: {exception.Message}");
|
||||
Log($"Coverage analysis failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -244,4 +294,4 @@ public sealed class CoverageViewModel : ObservableObject
|
||||
|
||||
LogLines.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using gregExtractor.Models;
|
||||
using gregExtractor.Services;
|
||||
using gregExtractor.Utils;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
public sealed class CreateViewModel : ObservableObject
|
||||
public sealed class CreateViewModel : BaseViewModel
|
||||
{
|
||||
private readonly TemplateService _templateService = new();
|
||||
|
||||
@@ -21,8 +23,7 @@ public sealed class CreateViewModel : ObservableObject
|
||||
|
||||
public CreateViewModel()
|
||||
{
|
||||
string groupsPath = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json");
|
||||
HookClassifier classifier = HookClassifier.LoadFromFile(groupsPath, new Progress<string>(AddLog));
|
||||
HookClassifier classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(AddLog));
|
||||
foreach (string group in classifier.Groups.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
Categories.Add(group);
|
||||
|
||||
@@ -51,8 +52,16 @@ public sealed class CreateViewModel : ObservableObject
|
||||
get => _isLoading;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isLoading, value))
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
if (!SetProperty(ref _isLoading, value))
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => SetProperty(ref _isLoading, value));
|
||||
return;
|
||||
}
|
||||
|
||||
NextStepCommand.NotifyCanExecuteChanged();
|
||||
PreviousStepCommand.NotifyCanExecuteChanged();
|
||||
@@ -95,7 +104,7 @@ public sealed class CreateViewModel : ObservableObject
|
||||
get => _outputPath;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _outputPath, value))
|
||||
if (!SetProperty(ref _outputPath, value?.Trim() ?? string.Empty))
|
||||
return;
|
||||
|
||||
OpenOutputCommand.NotifyCanExecuteChanged();
|
||||
@@ -160,27 +169,28 @@ public sealed class CreateViewModel : ObservableObject
|
||||
try
|
||||
{
|
||||
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
|
||||
TemplateGenerationResult result = await _templateService
|
||||
.GenerateModAsync(
|
||||
ModName,
|
||||
SelectedCategory,
|
||||
SelectedTemplateType,
|
||||
OutputPath,
|
||||
hooksPath,
|
||||
new Progress<string>(AddLog),
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
var result = await Task.Run(() => _templateService.GenerateModAsync(
|
||||
ModName,
|
||||
SelectedCategory,
|
||||
SelectedTemplateType,
|
||||
OutputPath,
|
||||
hooksPath,
|
||||
new UIProgress<string>(AddLog),
|
||||
CancellationToken.None)).ConfigureAwait(false);
|
||||
|
||||
PreviewTree.Clear();
|
||||
PreviewTree.Add(Path.GetFileName(result.ModDirectory) + "/");
|
||||
foreach (string file in result.GeneratedFiles.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
PreviewTree.Add(" " + Path.GetRelativePath(result.ModDirectory, file).Replace('\\', '/'));
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
PreviewTree.Clear();
|
||||
PreviewTree.Add(Path.GetFileName(result.ModDirectory) + "/");
|
||||
foreach (string file in result.GeneratedFiles.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
PreviewTree.Add(" " + Path.GetRelativePath(result.ModDirectory, file).Replace('\\', '/'));
|
||||
});
|
||||
|
||||
AddLog($"✓ Mod erstellt: {result.ModDirectory}");
|
||||
AddLog($"Mod erstellt: {result.ModDirectory}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"✗ Mod-Erstellung fehlgeschlagen: {exception.Message}");
|
||||
AddLog($"Mod-Erstellung fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -202,12 +212,15 @@ public sealed class CreateViewModel : ObservableObject
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
PreviewTree.Clear();
|
||||
PreviewTree.Add(ModName + "/");
|
||||
PreviewTree.Add(" " + ModName + ".csproj");
|
||||
PreviewTree.Add(" MainPlugin.cs");
|
||||
PreviewTree.Add(" mod.json");
|
||||
PreviewTree.Add(" Patches/");
|
||||
RunOnUIThread(() =>
|
||||
{
|
||||
PreviewTree.Clear();
|
||||
PreviewTree.Add(ModName + "/");
|
||||
PreviewTree.Add(" " + ModName + ".csproj");
|
||||
PreviewTree.Add(" MainPlugin.cs");
|
||||
PreviewTree.Add(" mod.json");
|
||||
PreviewTree.Add(" Patches/");
|
||||
});
|
||||
}
|
||||
|
||||
private void AddLog(string message)
|
||||
@@ -218,4 +231,4 @@ public sealed class CreateViewModel : ObservableObject
|
||||
|
||||
LogLines.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using gregExtractor.Models;
|
||||
@@ -6,7 +7,7 @@ using gregExtractor.Utils;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
public sealed class ExtractorViewModel : ObservableObject
|
||||
public sealed class ExtractorViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ExtractorService _extractorService = new();
|
||||
private HookClassifier _classifier;
|
||||
@@ -18,8 +19,7 @@ public sealed class ExtractorViewModel : ObservableObject
|
||||
|
||||
public ExtractorViewModel()
|
||||
{
|
||||
string groupsPath = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json");
|
||||
_classifier = HookClassifier.LoadFromFile(groupsPath, new Progress<string>(AddLog));
|
||||
_classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(AddLog));
|
||||
|
||||
foreach ((string groupName, HookGroupConfig config) in _classifier.Groups)
|
||||
HookGroups.Add(new HookGroupViewModel(groupName, config.Description));
|
||||
@@ -50,8 +50,19 @@ public sealed class ExtractorViewModel : ObservableObject
|
||||
get => _isLoading;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isLoading, value))
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
if (!SetProperty(ref _isLoading, value))
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
SetProperty(ref _isLoading, value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
StartAnalysisCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
@@ -87,9 +98,9 @@ public sealed class ExtractorViewModel : ObservableObject
|
||||
Il2CppPath = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Il2CppPath))
|
||||
AddLog("⚠ Kein IL2CPP-Pfad automatisch erkannt.");
|
||||
AddLog("Kein IL2CPP-Pfad automatisch erkannt.");
|
||||
else
|
||||
AddLog($"✓ IL2CPP-Pfad erkannt: {Il2CppPath}");
|
||||
AddLog($"IL2CPP-Pfad erkannt: {Il2CppPath}");
|
||||
}
|
||||
|
||||
private async Task BrowsePathAsync(string? target)
|
||||
@@ -111,25 +122,41 @@ public sealed class ExtractorViewModel : ObservableObject
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Il2CppPath) || !Directory.Exists(Il2CppPath))
|
||||
{
|
||||
AddLog("✗ IL2CPP-Pfad ungültig.");
|
||||
AddLog("IL2CPP-Pfad ungueltig.");
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
Progress = 0;
|
||||
|
||||
var progress = new Progress<string>(message =>
|
||||
var progress = new UIProgress<string>(msg =>
|
||||
{
|
||||
AddLog(message);
|
||||
AddLog(msg);
|
||||
Progress = Math.Min(98, Progress + 1);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
IReadOnlyList<HookDefinition> hooks = await _extractorService
|
||||
var result = await _extractorService
|
||||
.ExtractAsync(Il2CppPath, _classifier, progress, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result.IsCancelled)
|
||||
{
|
||||
AddLog("Scan abgebrochen.");
|
||||
IsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
AddLog(result.Error ?? "Unbekannter Fehler");
|
||||
IsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var hooks = result.Hooks;
|
||||
|
||||
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
|
||||
await File.WriteAllTextAsync(hooksPath, JsonSerializer.Serialize(hooks, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
@@ -139,22 +166,21 @@ public sealed class ExtractorViewModel : ObservableObject
|
||||
await File.WriteAllTextAsync(unknownPath, JsonSerializer.Serialize(unknown, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
ApplyGroups(hooks);
|
||||
await Dispatcher.UIThread.InvokeAsync(() => ApplyGroups(hooks));
|
||||
Progress = 100;
|
||||
AddLog($"✓ Analyse abgeschlossen. Hooks: {hooks.Count}");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
AddLog($"✗ Analyse fehlgeschlagen: {exception.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
AddLog($"Analyse abgeschlossen. Hooks: {hooks.Count}");
|
||||
IsLoading = false;
|
||||
|
||||
OnPropertyChanged(nameof(TotalHooks));
|
||||
OnPropertyChanged(nameof(GroupedHooks));
|
||||
OnPropertyChanged(nameof(UnknownHooks));
|
||||
OnPropertyChanged(nameof(GroupCount));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"Kritischer Fehler: {ex.Message}");
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyGroups(IEnumerable<HookDefinition> hooks)
|
||||
@@ -188,4 +214,4 @@ public sealed class ExtractorViewModel : ObservableObject
|
||||
|
||||
LogLines.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using gregExtractor.Models;
|
||||
using gregExtractor.Services;
|
||||
using gregExtractor.Utils;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
@@ -15,14 +18,19 @@ public sealed class HookBrowserRow
|
||||
public required string Parameters { get; init; }
|
||||
}
|
||||
|
||||
public sealed class HookBrowserViewModel : ObservableObject
|
||||
public sealed class HookBrowserViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ExtractorService _extractorService = new();
|
||||
private HookClassifier _classifier;
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
private string _selectedGroup = "Alle";
|
||||
private bool _isLoading;
|
||||
|
||||
public HookBrowserViewModel()
|
||||
{
|
||||
_classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(_ => { }));
|
||||
|
||||
Groups.Add("Alle");
|
||||
RefreshCommand = new AsyncRelayCommand(RefreshAsync, () => !IsLoading);
|
||||
}
|
||||
@@ -35,7 +43,7 @@ public sealed class HookBrowserViewModel : ObservableObject
|
||||
if (!SetProperty(ref _searchText, value))
|
||||
return;
|
||||
|
||||
ApplyFilter();
|
||||
RunOnUIThread(ApplyFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +55,7 @@ public sealed class HookBrowserViewModel : ObservableObject
|
||||
if (!SetProperty(ref _selectedGroup, value))
|
||||
return;
|
||||
|
||||
ApplyFilter();
|
||||
RunOnUIThread(ApplyFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,42 +81,65 @@ public sealed class HookBrowserViewModel : ObservableObject
|
||||
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
_classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(_ => { }));
|
||||
|
||||
string? il2CppPath = SteamLocator.FindIl2CppAssembliesPath();
|
||||
if (string.IsNullOrWhiteSpace(il2CppPath))
|
||||
{
|
||||
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
|
||||
if (!File.Exists(hooksPath))
|
||||
RunOnUIThread(() =>
|
||||
{
|
||||
Rows.Clear();
|
||||
FilteredRows.Clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
var result = await _extractorService.ExtractAsync(
|
||||
il2CppPath,
|
||||
_classifier,
|
||||
new UIProgress<string>(_ => { }),
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
RunOnUIThread(() =>
|
||||
{
|
||||
Rows.Clear();
|
||||
FilteredRows.Clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
string json = await File.ReadAllTextAsync(hooksPath, CancellationToken.None).ConfigureAwait(false);
|
||||
HookDefinition[] hooks = JsonSerializer.Deserialize<HookDefinition[]>(json) ?? Array.Empty<HookDefinition>();
|
||||
var hooks = result.Hooks;
|
||||
|
||||
Rows.Clear();
|
||||
foreach (HookDefinition hook in hooks)
|
||||
await RunOnUIThreadAsync(() =>
|
||||
{
|
||||
Rows.Add(new HookBrowserRow
|
||||
Rows.Clear();
|
||||
foreach (HookDefinition hook in hooks)
|
||||
{
|
||||
Group = hook.Group,
|
||||
Namespace = hook.Namespace,
|
||||
ClassName = hook.ClassName,
|
||||
MethodName = hook.MethodName,
|
||||
ReturnType = hook.ReturnType,
|
||||
IsVoid = hook.IsVoid,
|
||||
Parameters = string.Join(", ", hook.Parameters.Select(p => $"{p.Type} {p.Name}")),
|
||||
});
|
||||
}
|
||||
Rows.Add(new HookBrowserRow
|
||||
{
|
||||
Group = hook.Group,
|
||||
Namespace = hook.Namespace,
|
||||
ClassName = hook.ClassName,
|
||||
MethodName = hook.MethodName,
|
||||
ReturnType = hook.ReturnType,
|
||||
IsVoid = hook.IsVoid,
|
||||
Parameters = string.Join(", ", hook.Parameters.Select(p => $"{p.Type} {p.Name}")),
|
||||
});
|
||||
}
|
||||
|
||||
HashSet<string> groups = hooks.Select(h => h.Group).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
Groups.Clear();
|
||||
Groups.Add("Alle");
|
||||
foreach (string group in groups.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
Groups.Add(group);
|
||||
HashSet<string> groups = hooks.Select(h => h.Group).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
Groups.Clear();
|
||||
Groups.Add("Alle");
|
||||
foreach (string group in groups.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
Groups.Add(group);
|
||||
|
||||
ApplyFilter();
|
||||
ApplyFilter();
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -136,4 +167,4 @@ public sealed class HookBrowserViewModel : ObservableObject
|
||||
foreach (HookBrowserRow row in query)
|
||||
FilteredRows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
public sealed class MainViewModel : ObservableObject
|
||||
public sealed class MainViewModel : BaseViewModel
|
||||
{
|
||||
private NavigationItemViewModel? _selectedNavigationItem;
|
||||
private object? _currentPageViewModel;
|
||||
|
||||
@@ -4,7 +4,7 @@ using gregExtractor.Utils;
|
||||
|
||||
namespace gregExtractor.GUI.ViewModels;
|
||||
|
||||
public sealed class SettingsViewModel : ObservableObject
|
||||
public sealed class SettingsViewModel : BaseViewModel
|
||||
{
|
||||
private string _gamePath = SteamLocator.FindGamePath() ?? string.Empty;
|
||||
private string _frameworkSourcePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "gregCore", "src");
|
||||
@@ -31,19 +31,19 @@ public sealed class SettingsViewModel : ObservableObject
|
||||
public string GamePath
|
||||
{
|
||||
get => _gamePath;
|
||||
set => SetProperty(ref _gamePath, value);
|
||||
set => SetProperty(ref _gamePath, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string FrameworkSourcePath
|
||||
{
|
||||
get => _frameworkSourcePath;
|
||||
set => SetProperty(ref _frameworkSourcePath, value);
|
||||
set => SetProperty(ref _frameworkSourcePath, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public string ModsOutputPath
|
||||
{
|
||||
get => _modsOutputPath;
|
||||
set => SetProperty(ref _modsOutputPath, value);
|
||||
set => SetProperty(ref _modsOutputPath, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public bool IgnoreGettersSetters
|
||||
@@ -146,4 +146,4 @@ public sealed class SettingsViewModel : ObservableObject
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using gregExtractor.Models;
|
||||
@@ -23,7 +24,7 @@ public sealed record HookDiffEntry(
|
||||
public string Display => $"{ClassName}::{MethodName}";
|
||||
}
|
||||
|
||||
public sealed class SyncViewModel : ObservableObject
|
||||
public sealed class SyncViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ExtractorService _extractorService = new();
|
||||
private readonly DiffService _diffService = new();
|
||||
@@ -51,7 +52,7 @@ public sealed class SyncViewModel : ObservableObject
|
||||
public string FrameworkSourcePath
|
||||
{
|
||||
get => _frameworkSourcePath;
|
||||
set => SetProperty(ref _frameworkSourcePath, value);
|
||||
set => SetProperty(ref _frameworkSourcePath, value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public bool IsDryRun
|
||||
@@ -77,12 +78,25 @@ public sealed class SyncViewModel : ObservableObject
|
||||
get => _isRunning;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isRunning, value))
|
||||
return;
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
if (!SetProperty(ref _isRunning, value))
|
||||
return;
|
||||
|
||||
CalculateDiffCommand.NotifyCanExecuteChanged();
|
||||
RunSyncCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
CalculateDiffCommand.NotifyCanExecuteChanged();
|
||||
RunSyncCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
SetProperty(ref _isRunning, value);
|
||||
CalculateDiffCommand.NotifyCanExecuteChanged();
|
||||
RunSyncCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +137,7 @@ public sealed class SyncViewModel : ObservableObject
|
||||
{
|
||||
if (!Directory.Exists(FrameworkSourcePath))
|
||||
{
|
||||
AddLog("✗ Framework-Source-Pfad ist ungültig.");
|
||||
AddLog("Framework-Source-Pfad ist ungueltig.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,44 +145,49 @@ public sealed class SyncViewModel : ObservableObject
|
||||
|
||||
if (diff.Added.Count == 0 && diff.Changed.Count == 0 && diff.Removed.Count == 0)
|
||||
{
|
||||
AddLog("= Keine Änderungen erkannt. Sync übersprungen.");
|
||||
AddLog("Keine Aenderungen erkannt. Sync uebersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRunning = true;
|
||||
Warnings.Clear();
|
||||
|
||||
RunOnUIThread(() => Warnings.Clear());
|
||||
|
||||
try
|
||||
{
|
||||
HookDefinition[] allHooks = await LoadNewHooksAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
var progress = new Progress<string>(AddLog);
|
||||
var progress = new UIProgress<string>(AddLog);
|
||||
|
||||
SyncResult result = _syncService.Sync(new SyncOptions(
|
||||
var result = await Task.Run(() => _syncService.Sync(new SyncOptions(
|
||||
FrameworkSourceDir: FrameworkSourcePath,
|
||||
Diff: diff,
|
||||
AllHooks: allHooks.ToList(),
|
||||
DryRun: IsDryRun,
|
||||
Progress: progress));
|
||||
Progress: progress)), CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
LastResult = result;
|
||||
foreach (string warning in result.Warnings)
|
||||
Warnings.Add(warning);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
foreach (string warning in result.Warnings)
|
||||
Warnings.Add(warning);
|
||||
});
|
||||
|
||||
AddLog($"✓ Sync abgeschlossen. Dateien: {result.FilesWritten.Count}, +{result.LinesAdded}/-{result.LinesRemoved} Zeilen");
|
||||
AddLog($"Sync abgeschlossen. Dateien: {result.FilesWritten.Count}, +{result.LinesAdded}/-{result.LinesRemoved} Zeilen");
|
||||
|
||||
if (!IsDryRun)
|
||||
{
|
||||
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
|
||||
await File.WriteAllTextAsync(hooksPath, JsonSerializer.Serialize(allHooks, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None).ConfigureAwait(false);
|
||||
AddLog($"✓ game_hooks.json aktualisiert: {hooksPath}");
|
||||
AddLog($"game_hooks.json aktualisiert: {hooksPath}");
|
||||
}
|
||||
|
||||
if (UseGitDiff)
|
||||
await AppendGitDiffStatAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"✗ Sync fehlgeschlagen: {exception.Message}");
|
||||
AddLog($"Sync fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -179,8 +198,12 @@ public sealed class SyncViewModel : ObservableObject
|
||||
private async Task<HookDiff> ComputeDiffAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
IsRunning = true;
|
||||
DiffEntries.Clear();
|
||||
Warnings.Clear();
|
||||
|
||||
RunOnUIThread(() =>
|
||||
{
|
||||
DiffEntries.Clear();
|
||||
Warnings.Clear();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
@@ -190,21 +213,24 @@ public sealed class SyncViewModel : ObservableObject
|
||||
HookDiff diff = _diffService.Diff(oldHooks.ToList(), newHooks.ToList());
|
||||
_lastDiff = diff;
|
||||
|
||||
foreach (HookDefinition hook in diff.Added)
|
||||
DiffEntries.Add(ToEntry(HookDiffKind.Added, hook));
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
foreach (HookDefinition hook in diff.Added)
|
||||
DiffEntries.Add(ToEntry(HookDiffKind.Added, hook));
|
||||
|
||||
foreach (HookDefinition hook in diff.Changed)
|
||||
DiffEntries.Add(ToEntry(HookDiffKind.Changed, hook));
|
||||
foreach (HookDefinition hook in diff.Changed)
|
||||
DiffEntries.Add(ToEntry(HookDiffKind.Changed, hook));
|
||||
|
||||
foreach (HookDefinition hook in diff.Removed)
|
||||
DiffEntries.Add(ToEntry(HookDiffKind.Removed, hook));
|
||||
foreach (HookDefinition hook in diff.Removed)
|
||||
DiffEntries.Add(ToEntry(HookDiffKind.Removed, hook));
|
||||
});
|
||||
|
||||
AddLog($"Diff berechnet: +{diff.Added.Count} ~{diff.Changed.Count} -{diff.Removed.Count}");
|
||||
return diff;
|
||||
}
|
||||
catch (Exception exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"✗ Diff-Berechnung fehlgeschlagen: {exception.Message}");
|
||||
AddLog($"Diff-Berechnung fehlgeschlagen: {ex.Message}");
|
||||
return new HookDiff(Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>());
|
||||
}
|
||||
finally
|
||||
@@ -225,18 +251,22 @@ public sealed class SyncViewModel : ObservableObject
|
||||
|
||||
private async Task<HookDefinition[]> LoadNewHooksAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string groupsPath = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json");
|
||||
HookClassifier classifier = HookClassifier.LoadFromFile(groupsPath, new Progress<string>(AddLog));
|
||||
HookClassifier classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(AddLog));
|
||||
|
||||
string? il2CppPath = SteamLocator.FindIl2CppAssembliesPath();
|
||||
if (string.IsNullOrWhiteSpace(il2CppPath) || !Directory.Exists(il2CppPath))
|
||||
throw new DirectoryNotFoundException("IL2CPP-Pfad konnte nicht automatisch gefunden werden.");
|
||||
|
||||
IReadOnlyList<HookDefinition> hooks = await _extractorService
|
||||
.ExtractAsync(il2CppPath, classifier, new Progress<string>(AddLog), cancellationToken)
|
||||
il2CppPath = il2CppPath.Trim();
|
||||
|
||||
var result = await _extractorService
|
||||
.ExtractAsync(il2CppPath, classifier, new UIProgress<string>(AddLog), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return hooks.ToArray();
|
||||
if (!result.IsSuccess)
|
||||
throw new InvalidOperationException(result.Error ?? "Extraction failed");
|
||||
|
||||
return result.Hooks.ToArray();
|
||||
}
|
||||
|
||||
private async Task AppendGitDiffStatAsync(CancellationToken cancellationToken)
|
||||
@@ -264,7 +294,7 @@ public sealed class SyncViewModel : ObservableObject
|
||||
AddLog(output.Trim());
|
||||
|
||||
if (process.ExitCode != 0 && !string.IsNullOrWhiteSpace(error))
|
||||
AddLog("⚠ " + error.Trim());
|
||||
AddLog(error.Trim());
|
||||
}
|
||||
|
||||
private void OpenBackupFolder()
|
||||
@@ -293,4 +323,4 @@ public sealed class SyncViewModel : ObservableObject
|
||||
|
||||
SyncLog.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace gregExtractor.Models;
|
||||
|
||||
public sealed record ExtractorResult
|
||||
{
|
||||
public bool IsSuccess { get; init; }
|
||||
public bool IsCancelled { get; init; }
|
||||
public string? Error { get; init; }
|
||||
public List<HookDefinition> Hooks { get; init; } = new();
|
||||
|
||||
public static ExtractorResult Success(List<HookDefinition> hooks)
|
||||
=> new() { IsSuccess = true, Hooks = hooks };
|
||||
|
||||
public static ExtractorResult Failure(string error)
|
||||
=> new() { IsSuccess = false, Error = error };
|
||||
|
||||
public static ExtractorResult Cancelled()
|
||||
=> new() { IsCancelled = true };
|
||||
}
|
||||
+184
-94
@@ -1,140 +1,230 @@
|
||||
using gregExtractor.Models;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace gregExtractor.Services;
|
||||
|
||||
public sealed class ExtractorService
|
||||
{
|
||||
public async Task<IReadOnlyList<HookDefinition>> ExtractAsync(
|
||||
public async Task<ExtractorResult> ExtractAsync(
|
||||
string il2CppDir,
|
||||
HookClassifier classifier,
|
||||
IProgress<string>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
IProgress<string>? progress = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(il2CppDir) || !Directory.Exists(il2CppDir))
|
||||
throw new DirectoryNotFoundException($"IL2CPP directory not found: {il2CppDir}");
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
string assemblyPath = Path.Combine(il2CppDir, "Assembly-CSharp.dll");
|
||||
if (!File.Exists(assemblyPath))
|
||||
throw new FileNotFoundException("Assembly-CSharp.dll was not found.", assemblyPath);
|
||||
if (string.IsNullOrWhiteSpace(il2CppDir) || !Directory.Exists(il2CppDir))
|
||||
return ExtractorResult.Failure($"IL2CPP directory not found: {il2CppDir}");
|
||||
|
||||
return await Task.Run(() => ExtractInternal(assemblyPath, il2CppDir, classifier, progress, cancellationToken), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var assemblyPath = Path.Combine(il2CppDir, "Assembly-CSharp.dll");
|
||||
if (!File.Exists(assemblyPath))
|
||||
return ExtractorResult.Failure($"Assembly-CSharp.dll not found in:\n{il2CppDir}");
|
||||
|
||||
progress?.Report("Lade Assembly...");
|
||||
|
||||
using var assembly = LoadAssemblySafe(assemblyPath, il2CppDir, progress);
|
||||
if (assembly is null)
|
||||
return ExtractorResult.Failure("Assembly konnte nicht geladen werden. Siehe crash.log.");
|
||||
|
||||
progress?.Report("Scanne Typen...");
|
||||
|
||||
var hooks = ScanTypesDefensive(assembly, classifier, progress, ct);
|
||||
|
||||
progress?.Report($"✓ {hooks.Count} Hooks gefunden.");
|
||||
return ExtractorResult.Success(hooks);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return ExtractorResult.Cancelled();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var crashLog = Path.Combine(AppContext.BaseDirectory, "crash.log");
|
||||
File.AppendAllText(crashLog,
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] SCAN FEHLER\n{ex}\n\n");
|
||||
|
||||
return ExtractorResult.Failure($"Scan-Fehler: {ex.Message}\nDetails: crash.log");
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<HookDefinition> ExtractInternal(
|
||||
private static AssemblyDefinition? LoadAssemblySafe(
|
||||
string assemblyPath,
|
||||
string il2CppDir,
|
||||
HookClassifier classifier,
|
||||
IProgress<string>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
IProgress<string>? progress)
|
||||
{
|
||||
var resolver = new DefaultAssemblyResolver();
|
||||
resolver.AddSearchDirectory(il2CppDir);
|
||||
|
||||
foreach (string directory in Directory.EnumerateDirectories(il2CppDir))
|
||||
resolver.AddSearchDirectory(directory);
|
||||
string? melonDir = Path.GetDirectoryName(il2CppDir);
|
||||
if (melonDir is not null)
|
||||
{
|
||||
resolver.AddSearchDirectory(melonDir);
|
||||
|
||||
var readerParameters = new ReaderParameters
|
||||
string net6Dir = Path.Combine(melonDir, "net6");
|
||||
if (Directory.Exists(net6Dir))
|
||||
resolver.AddSearchDirectory(net6Dir);
|
||||
}
|
||||
|
||||
foreach (string dir in Directory.EnumerateDirectories(il2CppDir))
|
||||
resolver.AddSearchDirectory(dir);
|
||||
|
||||
var readerParams = new ReaderParameters
|
||||
{
|
||||
AssemblyResolver = resolver,
|
||||
ReadingMode = ReadingMode.Deferred,
|
||||
ReadSymbols = false,
|
||||
ThrowIfSymbolsAreNotMatching = false,
|
||||
};
|
||||
|
||||
progress?.Report("Loading Assembly-CSharp.dll via Mono.Cecil...");
|
||||
progress?.Report($"Loading: {Path.GetFileName(assemblyPath)}");
|
||||
|
||||
using AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters);
|
||||
var hooks = new List<HookDefinition>(capacity: 8192);
|
||||
|
||||
foreach (TypeDefinition type in assembly.MainModule.Types)
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return AssemblyDefinition.ReadAssembly(assemblyPath, readerParams);
|
||||
}
|
||||
catch (AssemblyResolutionException ex)
|
||||
{
|
||||
progress?.Report($"Referenz fehlt: {ex.AssemblyReference.Name}");
|
||||
return LoadAssemblyWithoutResolver(assemblyPath, progress);
|
||||
}
|
||||
catch (BadImageFormatException ex)
|
||||
{
|
||||
progress?.Report($"Ungültiges Format: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
progress?.Report($"IO-Fehler: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
progress?.Report($"InvalidOperation: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!type.IsPublic || type.IsInterface)
|
||||
continue;
|
||||
private static AssemblyDefinition? LoadAssemblyWithoutResolver(
|
||||
string assemblyPath,
|
||||
IProgress<string>? progress)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AssemblyDefinition.ReadAssembly(assemblyPath,
|
||||
new ReaderParameters { ReadingMode = ReadingMode.Deferred });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
progress?.Report($"Fallback-Load fehlgeschlagen: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
string className = type.Name;
|
||||
string namespaceName = type.Namespace;
|
||||
private static List<HookDefinition> ScanTypesDefensive(
|
||||
AssemblyDefinition assembly,
|
||||
HookClassifier classifier,
|
||||
IProgress<string>? progress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var results = new List<HookDefinition>();
|
||||
var types = assembly.MainModule.Types.ToList();
|
||||
var total = types.Count;
|
||||
|
||||
foreach (MethodDefinition method in type.Methods)
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (i % 100 == 0)
|
||||
progress?.Report($"Scanne {i}/{total} Typen...");
|
||||
|
||||
var type = types[i];
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!IsValidType(type)) continue;
|
||||
|
||||
if (!method.IsPublic)
|
||||
continue;
|
||||
foreach (var method in type.Methods)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsValidMethod(method)) continue;
|
||||
|
||||
if (method.IsConstructor || method.IsSpecialName)
|
||||
continue;
|
||||
|
||||
if (!method.Parameters.Any() && method.ReturnType.FullName.Equals("System.Void", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var parameters = method.Parameters
|
||||
.Select(parameter => new HookParameter(parameter.Name, NormalizeTypeName(parameter.ParameterType)))
|
||||
.ToArray();
|
||||
|
||||
string returnType = NormalizeTypeName(method.ReturnType);
|
||||
bool isVoid = string.Equals(returnType, "void", StringComparison.OrdinalIgnoreCase);
|
||||
string group = classifier.Classify(className, method.Name);
|
||||
|
||||
hooks.Add(new HookDefinition(
|
||||
Group: group,
|
||||
Namespace: namespaceName,
|
||||
ClassName: className,
|
||||
MethodName: method.Name,
|
||||
ReturnType: returnType,
|
||||
IsVoid: isVoid,
|
||||
Parameters: parameters));
|
||||
var hook = BuildHookDefinition(type, method, classifier);
|
||||
results.Add(hook);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
progress?.Report($"⚠ Methode übersprungen: {type.Name}.{method?.Name} — {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
progress?.Report($"⚠ Typ übersprungen: {type?.Name} — {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
progress?.Report($"Extraction finished. Hooks found: {hooks.Count}");
|
||||
|
||||
return hooks
|
||||
.OrderBy(hook => hook.Group, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(hook => hook.ClassName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(hook => hook.MethodName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
return results.OrderBy(h => h.Group, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(h => h.ClassName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(h => h.MethodName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string NormalizeTypeName(TypeReference typeReference)
|
||||
private static HookDefinition BuildHookDefinition(
|
||||
TypeDefinition type,
|
||||
MethodDefinition method,
|
||||
HookClassifier classifier)
|
||||
{
|
||||
if (typeReference.IsByReference && typeReference is ByReferenceType byReferenceType)
|
||||
return NormalizeTypeName(byReferenceType.ElementType);
|
||||
var parameters = new List<HookParameter>();
|
||||
|
||||
if (typeReference is GenericInstanceType genericInstanceType)
|
||||
foreach (var param in method.Parameters)
|
||||
{
|
||||
string genericName = genericInstanceType.Name;
|
||||
int backtick = genericName.IndexOf('`');
|
||||
if (backtick >= 0)
|
||||
genericName = genericName[..backtick];
|
||||
|
||||
string genericArgs = string.Join(", ", genericInstanceType.GenericArguments.Select(NormalizeTypeName));
|
||||
return $"{genericName}<{genericArgs}>";
|
||||
string paramType = param.ParameterType?.Name ?? "object";
|
||||
string paramName = string.IsNullOrWhiteSpace(param.Name)
|
||||
? $"arg{param.Index}"
|
||||
: param.Name;
|
||||
parameters.Add(new HookParameter(paramName, paramType));
|
||||
}
|
||||
|
||||
if (typeReference.IsArray && typeReference is ArrayType arrayType)
|
||||
return $"{NormalizeTypeName(arrayType.ElementType)}[]";
|
||||
string returnTypeName = method.ReturnType?.Name ?? "object";
|
||||
string group = classifier.Classify(type.Name, method.Name);
|
||||
string ns = type.Namespace ?? string.Empty;
|
||||
|
||||
return typeReference.FullName switch
|
||||
{
|
||||
"System.Void" => "void",
|
||||
"System.Boolean" => "bool",
|
||||
"System.Byte" => "byte",
|
||||
"System.SByte" => "sbyte",
|
||||
"System.Int16" => "short",
|
||||
"System.UInt16" => "ushort",
|
||||
"System.Int32" => "int",
|
||||
"System.UInt32" => "uint",
|
||||
"System.Int64" => "long",
|
||||
"System.UInt64" => "ulong",
|
||||
"System.Single" => "float",
|
||||
"System.Double" => "double",
|
||||
"System.Decimal" => "decimal",
|
||||
"System.String" => "string",
|
||||
"System.Char" => "char",
|
||||
"System.Object" => "object",
|
||||
_ => typeReference.Name,
|
||||
};
|
||||
return new HookDefinition(
|
||||
Group: group,
|
||||
Namespace: ns,
|
||||
ClassName: type.Name,
|
||||
MethodName: method.Name,
|
||||
ReturnType: returnTypeName,
|
||||
IsVoid: string.Equals(returnTypeName, "Void", StringComparison.OrdinalIgnoreCase),
|
||||
Parameters: parameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidType(TypeDefinition? type)
|
||||
{
|
||||
if (type is null) return false;
|
||||
if (!type.IsPublic) return false;
|
||||
if (type.IsInterface) return false;
|
||||
if (string.IsNullOrWhiteSpace(type.Name)) return false;
|
||||
if (type.Name.StartsWith('<')) return false;
|
||||
if (type.Name.StartsWith("__", StringComparison.Ordinal)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidMethod(MethodDefinition? method)
|
||||
{
|
||||
if (method is null) return false;
|
||||
if (!method.IsPublic) return false;
|
||||
if (method.IsConstructor) return false;
|
||||
if (method.IsSpecialName) return false;
|
||||
if (string.IsNullOrWhiteSpace(method.Name)) return false;
|
||||
if (method.Name.StartsWith('<')) return false;
|
||||
if (method.ReturnType is null) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+64
-14
@@ -6,22 +6,50 @@ public sealed class HookClassifier
|
||||
{
|
||||
private static readonly Dictionary<string, string[]> MethodKeywordMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Persistence"] = ["save", "load", "autosave"],
|
||||
["Economy"] = ["coin", "money", "shop", "buy", "xp", "reputation", "checkout"],
|
||||
["Networking"] = ["connect", "device", "cable", "ip", "network", "lacp"],
|
||||
["Hardware"] = ["power", "repair", "broken", "server", "switch"],
|
||||
["Lifecycle"] = ["pause", "resume", "day", "scene", "quit", "loadlevel"],
|
||||
["VisualUI"] = ["ui", "menu", "panel", "screen", "button", "hud"],
|
||||
["Persistence"] = ["save", "load", "autosave", "serialize", "deserialize", "generateid"],
|
||||
["Economy"] = ["coin", "money", "shop", "buy", "xp", "reputation", "checkout", "salary", "revenue", "balance", "app", "customer"],
|
||||
["Networking"] = ["connect", "device", "cable", "ip", "network", "lacp", "sfp"],
|
||||
["Hardware"] = ["power", "repair", "broken", "server", "switch", "technician", "sfpmodule", "insertport", "slideport"],
|
||||
["Lifecycle"] = ["pause", "resume", "day", "scene", "quit", "loadlevel", "objective", "operator", "spawn", "destroy"],
|
||||
["VisualUI"] = ["ui", "menu", "panel", "screen", "button", "hud", "pointer", "select", "submit", "color", "picker", "keyboard", "text", "osk", "scroll", "recycle"],
|
||||
["Audio"] = ["fade", "volume", "music", "audio", "sound"],
|
||||
["Character"] = ["move", "walk", "run", "animation", "mouth", "talk", "express", "controller", "input", "look", "waypoint"],
|
||||
["World"] = ["interact", "collision", "trigger", "animator", "environment", "dumpster", "rope", "usable", "trolley", "gate", "lever"],
|
||||
["Input"] = ["input", "key", "mouse", "viper", "invert"],
|
||||
["Steam"] = ["steam", "leaderboard"],
|
||||
["Settings"] = ["settings", "volume", "gameplay", "controls", "packet", "route", "language", "rect"],
|
||||
["Facility"] = ["wall", "openwall", "push", "container"],
|
||||
["CustomImport"] = ["modprefab", "creatematerial", "importobj"],
|
||||
["Interaction"] = ["interact", "labelaction", "secondaction", "hover"],
|
||||
["Tutorials"] = ["tutorial", "playvideo", "showtutorial", "stoptutorial"],
|
||||
["DevTools"] = ["godmod", "cheat", "debug"],
|
||||
["Maintenance"] = ["technician", "repair", "dispatch", "job"],
|
||||
["GameState"] = ["commandcenter", "operator", "fcp"],
|
||||
["Serialization"] = ["hashcode", "equals", "typeindex", "static", "assembly"],
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string[]> ClassKeywordMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Persistence"] = ["save"],
|
||||
["Economy"] = ["player", "shop", "economy"],
|
||||
["Persistence"] = ["save", "serial", "fcp"],
|
||||
["Economy"] = ["player", "shop", "economy", "balance", "customer", "bill"],
|
||||
["Networking"] = ["network", "cable", "packet", "switch", "server"],
|
||||
["Hardware"] = ["server", "rack", "item", "asset"],
|
||||
["Lifecycle"] = ["loading", "pause", "time", "scene", "playermanager"],
|
||||
["VisualUI"] = ["menu", "ui", "hud", "panel"],
|
||||
["Hardware"] = ["server", "rack", "item", "asset", "sfpbox", "setip", "technician", "sfpmodule"],
|
||||
["Lifecycle"] = ["loading", "pause", "time", "scene", "playermanager", "objective", "commandcenter", "operator"],
|
||||
["VisualUI"] = ["menu", "ui", "hud", "panel", "button", "color", "picker", "osk", "settingsgraphics", "tmp", "scrollrect", "recycling"],
|
||||
["Audio"] = ["audio", "music", "sound", "fade"],
|
||||
["Character"] = ["ai", "character", "npc", "firstperson", "expression", "mouseray", "waypoint"],
|
||||
["World"] = ["world", "env", "dumpster", "prop", "car", "controller", "rope", "usable", "trolley", "gate", "lever"],
|
||||
["Input"] = ["input", "viper", "keyboard", "key"],
|
||||
["Steam"] = ["steam"],
|
||||
["Settings"] = ["settings", "controls", "gameplay", "volume", "rect"],
|
||||
["Facility"] = ["wall", "trolley", "gate", "lever"],
|
||||
["CustomImport"] = ["modloader", "objimporter"],
|
||||
["Interaction"] = ["interact"],
|
||||
["Tutorials"] = ["tutorial"],
|
||||
["DevTools"] = ["godmod"],
|
||||
["Maintenance"] = ["technician"],
|
||||
["GameState"] = ["commandcenter", "fcp"],
|
||||
["Serialization"] = ["registry", "type", "assembly"],
|
||||
};
|
||||
|
||||
public HookClassifier(IReadOnlyDictionary<string, HookGroupConfig> groups)
|
||||
@@ -31,14 +59,18 @@ public sealed class HookClassifier
|
||||
|
||||
public IReadOnlyDictionary<string, HookGroupConfig> Groups { get; }
|
||||
|
||||
public static HookClassifier LoadFromFile(string jsonPath, IProgress<string>? progress = null)
|
||||
public static HookClassifier LoadFromFile(string? configuredPath, IProgress<string>? progress = null)
|
||||
{
|
||||
if (!File.Exists(jsonPath))
|
||||
string jsonPath = ResolveHookGroupsPath(configuredPath);
|
||||
|
||||
if (jsonPath is null)
|
||||
{
|
||||
progress?.Report($"hook_groups.json not found at '{jsonPath}'. Classifier starts with empty groups.");
|
||||
progress?.Report("hook_groups.json nicht gefunden. Classifier startet mit leeren Gruppen.");
|
||||
return new HookClassifier(new Dictionary<string, HookGroupConfig>(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
progress?.Report($"Lade hook_groups.json von: {jsonPath}");
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(jsonPath);
|
||||
@@ -71,6 +103,24 @@ public sealed class HookClassifier
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveHookGroupsPath(string? configuredPath)
|
||||
{
|
||||
if (configuredPath is not null && File.Exists(configuredPath))
|
||||
return configuredPath;
|
||||
|
||||
string exeDir = Path.GetDirectoryName(
|
||||
System.Reflection.Assembly.GetExecutingAssembly().Location)!;
|
||||
string nextToExe = Path.Combine(exeDir, "hook_groups.json");
|
||||
if (File.Exists(nextToExe))
|
||||
return nextToExe;
|
||||
|
||||
string inCwd = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json");
|
||||
if (File.Exists(inCwd))
|
||||
return inCwd;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string Classify(string className, string methodName)
|
||||
{
|
||||
// 1) Exact class + method match
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace gregExtractor.Utils;
|
||||
|
||||
public sealed class UIProgress<T> : IProgress<T>
|
||||
{
|
||||
private readonly Action<T> _handler;
|
||||
|
||||
public UIProgress(Action<T> handler)
|
||||
=> _handler = handler;
|
||||
|
||||
public void Report(T value)
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
_handler(value);
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => _handler(value));
|
||||
}
|
||||
}
|
||||
+83
-13
@@ -11,13 +11,13 @@
|
||||
},
|
||||
"Facility": {
|
||||
"description": "Building layouts, room expansions, floor designs",
|
||||
"classes": ["RoomManager", "BuildingManager", "FacilityManager", "FloorManager"],
|
||||
"methods": ["AddRoom", "RemoveRoom", "ExpandRoom", "PlaceObject", "RemoveObject", "UnlockArea"]
|
||||
"classes": ["RoomManager", "BuildingManager", "FacilityManager", "FloorManager", "Wall", "PushTrolleyHandle", "TrolleyTrigger", "GateLever"],
|
||||
"methods": ["AddRoom", "RemoveRoom", "ExpandRoom", "PlaceObject", "RemoveObject", "UnlockArea", "OpenWall", "OnHoverOver", "ObjectAdded", "OpenGate", "CloseGate", "TruckComing"]
|
||||
},
|
||||
"Hardware": {
|
||||
"description": "Custom servers, racks, switches, equipment",
|
||||
"classes": ["Server", "Rack", "Switch", "Item", "ShopItemSO", "AssetManagement"],
|
||||
"methods": ["PowerButton", "SetIP", "UpdateCustomer", "UpdateAppID", "IsBroken", "RepairDevice", "BuyNewItem"]
|
||||
"classes": ["Server", "Rack", "Switch", "Item", "ShopItemSO", "AssetManagement", "TechnicianManager", "SFPBox", "SetIP", "InputManager", "Technician", "SFPModule"],
|
||||
"methods": ["PowerButton", "SetIP", "UpdateCustomer", "UpdateAppID", "IsBroken", "RepairDevice", "BuyNewItem", "AddDevice", "RemoveDevice", "Connect", "Disconnect", "AssignJob", "RequestJobDelayed", "RotateTowardsGoal", "SendToContainer", "SetHandIKWeight", "InsertDirectlyIntoPort", "SlideIntoPort"]
|
||||
},
|
||||
"Networking": {
|
||||
"description": "Cable routing, packet visualization, network configs",
|
||||
@@ -31,27 +31,97 @@
|
||||
},
|
||||
"Economy": {
|
||||
"description": "Player economy, XP, shop transactions",
|
||||
"classes": ["Player", "ComputerShop"],
|
||||
"methods": ["UpdateCoin", "UpdateXP", "UpdateReputation", "ButtonCheckOut", "UpdateCartTotal"]
|
||||
"classes": ["Player", "ComputerShop", "BalanceSheet", "BalanceSheetRow", "CustomerBase", "CustomerBaseDoor", "CustomerCard", "CustomerColor"],
|
||||
"methods": ["UpdateCoin", "UpdateXP", "UpdateReputation", "ButtonCheckOut", "UpdateCartTotal", "AddRow", "AddSalaryRow", "AddSectionTitle", "AddTotalRow", "RegisterSalary", "TrackFinances", "AddAppPerformance", "AppText", "SetUpApp", "SetUpBase", "InteractOnHover", "OpenDoorAndSetupBase", "SetCustomer", "GetColorForCustomerId", "GetTotalAppSpeed", "GetSubnetsPerApp", "GetVlanIdsPerApp", "GetOrCreateRecord", "RestoreRecord", "InstantiateRow"]
|
||||
},
|
||||
"Persistence": {
|
||||
"description": "Save/Load game state",
|
||||
"classes": ["SaveSystem", "SaveData"],
|
||||
"methods": ["SaveGame", "LoadGame", "AutoSave", "Load", "SaveGameData", "LoadGameData"]
|
||||
"classes": ["SaveSystem", "SaveData", "ColorSerializationSurrogate", "FCP_Persistence"],
|
||||
"methods": ["SaveGame", "LoadGame", "AutoSave", "Load", "SaveGameData", "LoadGameData", "GetObjectData", "SetObjectData", "GenerateID"]
|
||||
},
|
||||
"VisualUI": {
|
||||
"description": "Interface overhauls, themes, visual enhancements",
|
||||
"classes": ["MainMenu", "PauseMenu", "UIManager", "HUDManager", "AssetManagement"],
|
||||
"methods": ["Continue", "NewGame", "Settings", "OpenPanel", "ClosePanel", "SetTheme"]
|
||||
"classes": ["MainMenu", "PauseMenu", "UIManager", "HUDManager", "AssetManagement", "ButtonExtended", "FlexibleColorPicker", "OSK_Keyboard", "OSK_Key", "OSK_GlyphHandler", "OSK_Keymap", "OSK_Receiver", "OSK_AccentConsole", "SettingsGraphics", "TMP_TextEventHandler", "TMP_TextSelector_B", "RecyclableScrollRect", "HorizontalRecyclingSystem", "VerticalRecyclingSystem", "RecyclingSystem", "AutoScrollRect"],
|
||||
"methods": ["Continue", "NewGame", "Settings", "OpenPanel", "ClosePanel", "SetTheme", "OnPointerClick", "OnPointerEnter", "OnPointerExit", "OnSelect", "OnSubmit", "OnDeselect", "ChangeMode", "GetColor", "SetColor", "ParseHex", "RGBToHSV", "HSVToRGB", "PickColor", "SetMarker", "GetValue", "IsValidHexChar", "GetNormalizedPointerPosition", "GetSanitizedHex", "Initialize", "OnValueChangedListener", "SetRecyclingBounds", "SetTopAnchor", "ScrollAuto"]
|
||||
},
|
||||
"Lifecycle": {
|
||||
"description": "Scene, load, pause, time events",
|
||||
"classes": ["LoadingScreen", "PauseMenu", "TimeController", "PlayerManager"],
|
||||
"methods": ["LoadGameScenes", "LoadLevel", "UnLoadLevel", "Pause", "Resume", "OnApplicationQuit"]
|
||||
"classes": ["LoadingScreen", "PauseMenu", "TimeController", "PlayerManager", "Objectives", "CommandCenter"],
|
||||
"methods": ["LoadGameScenes", "LoadLevel", "UnLoadLevel", "Pause", "Resume", "OnApplicationQuit", "SpawnOperatorsForLevel", "DestroyOperatorsForLevel", "SpawnOperatorsForSingleLevel", "ToggleClearWarningAuto"]
|
||||
},
|
||||
"Audio": {
|
||||
"description": "Audio management, music, sound effects",
|
||||
"classes": ["AudioManager"],
|
||||
"methods": ["FadeIn", "FadeOut", "FadeOut_FadeIn", "SetEffectsVolume", "SetMasterVolume", "SetMusic", "SetMusicVolume", "SetRacksVolume"]
|
||||
},
|
||||
"Character": {
|
||||
"description": "AI, NPCs, character movement and expressions",
|
||||
"classes": ["AICharacterControl", "AICharacterExpressions", "FirstPersonController", "MouseLook", "WaypointInitializationSystem", "RayLookAt"],
|
||||
"methods": ["AgentReachTarget", "AnimSit", "GotoNextPoint", "moveBack", "SetTarget", "Start", "MouthShape", "Talk", "Talking", "GetInput", "GetMouseLook", "OnControllerColliderHit", "ProgressStepCycle", "UpdateCameraPosition", "UpdateNormalFov", "LookAt"]
|
||||
},
|
||||
"World": {
|
||||
"description": "Environment, props, interactables",
|
||||
"classes": ["Dumpster", "EnvMapAnimator", "CheckIfTouchingWall", "CarController", "ActionKeyHint", "UsableObject", "Rope", "viperInput"],
|
||||
"methods": ["InteractOnHover", "DelayedOverlapCheck", "SetRenderersEnabled", "OnCollisionEnter", "ResetingTrollerPosition", "CustomKey", "DelayedAppDoorOpening", "AutoDisable"]
|
||||
},
|
||||
"Input": {
|
||||
"description": "Player input handling",
|
||||
"classes": ["InputManager", "viperInput"],
|
||||
"methods": ["GetInput", "OnInput", "OnKeyDown", "OnKeyUp", "OnMouseMove"]
|
||||
},
|
||||
"Steam": {
|
||||
"description": "Steam integration, leaderboards",
|
||||
"classes": ["SteamManager", "SteamLeaderboards"],
|
||||
"methods": ["InitOnPlayMode", "Init", "OnLeaderboardFound", "RequestUserEntry"]
|
||||
},
|
||||
"Settings": {
|
||||
"description": "Game settings and options",
|
||||
"classes": ["SettingsControls", "SettingsGameplay", "SettingsVolume", "SettingsSingleton", "RectExtensions"],
|
||||
"methods": ["InvertY", "SetPacketType", "SetRouteEvalInterval", "OnLanguageDropDownChange", "DisableOnAfterFirstSettingUp"]
|
||||
},
|
||||
"CustomImport": {
|
||||
"description": "Mod loading and custom asset importing",
|
||||
"classes": ["ModLoader", "ObjImporter"],
|
||||
"methods": ["GetModPrefab", "GetModPrefabByFolder", "CreateMaterial", "ImportOBJ"]
|
||||
},
|
||||
"Interaction": {
|
||||
"description": "Player interaction with world objects",
|
||||
"classes": ["Interact"],
|
||||
"methods": ["LabelActionOnClick", "SecondActionOnClick", "IsAllowedToDoSecondAction", "OnHoverOver"]
|
||||
},
|
||||
"Tutorials": {
|
||||
"description": "Tutorial system integration",
|
||||
"classes": ["Tutorials"],
|
||||
"methods": ["PlayVideo", "ShowTutorial", "StopTutorial", "OnEnable"]
|
||||
},
|
||||
"DevTools": {
|
||||
"description": "Developer tools and debug features",
|
||||
"classes": ["GODMOD"],
|
||||
"methods": ["StartGodMod", "GODMOD_delayed", "OnEnable", "OnDisable"]
|
||||
},
|
||||
"Maintenance": {
|
||||
"description": "Technician dispatch and repair jobs",
|
||||
"classes": ["Technician", "TechnicianManager"],
|
||||
"methods": ["AssignJob", "RequestJobDelayed", "RotateTowardsGoal", "SendToContainer", "SetHandIKWeight", "EnqueueDispatch", "GetActiveJobs", "GetQueuedJobs", "ProcessDispatchQueue", "RequestNextJob", "RestoreJobQueue"]
|
||||
},
|
||||
"GameState": {
|
||||
"description": "Game state and persistence",
|
||||
"classes": ["CommandCenter", "FCP_Persistence"],
|
||||
"methods": ["SpawnOperatorsForLevel", "DestroyOperatorsForLevel", "SpawnOperatorsForSingleLevel", "ToggleClearWarningAuto", "GenerateID", "OnEnable", "OnDisable"]
|
||||
},
|
||||
"Steam": {
|
||||
"description": "Steam integration, leaderboards",
|
||||
"classes": ["SteamManager", "SteamLeaderboards"],
|
||||
"methods": ["InitOnPlayMode", "Init", "OnLeaderboardFound", "RequestUserEntry"]
|
||||
},
|
||||
"Serialization": {
|
||||
"description": "Type registry, serialization helpers",
|
||||
"classes": ["AssemblyTypeRegistry"],
|
||||
"methods": ["BoxedGetHashCode", "Equals", "SetSharedStaticTypeIndices"]
|
||||
},
|
||||
"Misc": {
|
||||
"description": "Other utilities and catch-all hooks",
|
||||
"classes": [],
|
||||
"methods": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user