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:
Marvin
2026-04-20 04:26:06 +02:00
parent 948d8764c9
commit 2f61e5dc00
16 changed files with 773 additions and 287 deletions
+11 -4
View File
@@ -17,11 +17,10 @@ public sealed class ExtractCommand : AsyncCommand<ExtractCommand.Settings>
AnsiConsole.Write(new FigletText("gregExtractor").Color(Color.Cyan1)); AnsiConsole.Write(new FigletText("gregExtractor").Color(Color.Cyan1));
string workingDirectory = Directory.GetCurrentDirectory(); string workingDirectory = Directory.GetCurrentDirectory();
string groupsPath = Path.Combine(workingDirectory, "hook_groups.json");
var logMessages = new List<string>(); var logMessages = new List<string>();
var progress = new Progress<string>(message => logMessages.Add(message)); 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; string? il2CppPath = settings.Path;
if (string.IsNullOrWhiteSpace(il2CppPath)) if (string.IsNullOrWhiteSpace(il2CppPath))
@@ -39,17 +38,25 @@ public sealed class ExtractCommand : AsyncCommand<ExtractCommand.Settings>
return -1; return -1;
} }
IReadOnlyList<HookDefinition> hooks = Array.Empty<HookDefinition>();
var extractor = new ExtractorService(); var extractor = new ExtractorService();
ExtractorResult result = ExtractorResult.Failure("Not executed");
await AnsiConsole.Status() await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots) .Spinner(Spinner.Known.Dots)
.StartAsync("Extracting IL2CPP hooks...", async _ => .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); .ConfigureAwait(false);
if (!result.IsSuccess)
{
AnsiConsole.MarkupLine($"[red]✗ {result.Error}[/]");
return -1;
}
var hooks = result.Hooks;
string gameHooksPath = Path.Combine(workingDirectory, "game_hooks.json"); string gameHooksPath = Path.Combine(workingDirectory, "game_hooks.json");
await File.WriteAllTextAsync( await File.WriteAllTextAsync(
gameHooksPath, gameHooksPath,
+12 -13
View File
@@ -80,17 +80,16 @@ public sealed class SyncCommand : AsyncCommand<SyncCommand.Settings>
var logLines = new List<string>(); var logLines = new List<string>();
var progress = new Progress<string>(line => logLines.Add(line)); var progress = new Progress<string>(line => logLines.Add(line));
IReadOnlyList<HookDefinition> newHooks = Array.Empty<HookDefinition>(); var result = await extractor.ExtractAsync(il2CppPath, classifier, progress, CancellationToken.None).ConfigureAwait(false);
HookDiff diff = new(Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>());
await AnsiConsole.Status() if (!result.IsSuccess)
.Spinner(Spinner.Known.Dots) {
.StartAsync("Berechne Assembly-Diff...", async _ => AnsiConsole.MarkupLine($"[red]✗ {result.Error}[/]");
{ return -1;
newHooks = await extractor.ExtractAsync(il2CppPath, classifier, progress, CancellationToken.None).ConfigureAwait(false); }
diff = diffService.Diff(oldHooks.ToList(), newHooks.ToList());
}) var newHooks = result.Hooks;
.ConfigureAwait(false); HookDiff diff = diffService.Diff(oldHooks.ToList(), newHooks.ToList());
var summary = new Table().RoundedBorder(); var summary = new Table().RoundedBorder();
summary.Title = new TableTitle("Assembly Diff"); 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()); summary.AddRow("- Methoden entfernt", diff.Removed.Count.ToString());
AnsiConsole.Write(summary); AnsiConsole.Write(summary);
SyncResult result = syncService.Sync(new SyncOptions( SyncResult syncResult = syncService.Sync(new SyncOptions(
FrameworkSourceDir: settings.Source, FrameworkSourceDir: settings.Source,
Diff: diff, Diff: diff,
AllHooks: newHooks.ToList(), AllHooks: newHooks.ToList(),
@@ -111,10 +110,10 @@ public sealed class SyncCommand : AsyncCommand<SyncCommand.Settings>
foreach (string line in logLines) foreach (string line in logLines)
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(line)}[/]"); 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))}"); 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)}[/]"); AnsiConsole.MarkupLine($"[yellow]{Markup.Escape(warning)}[/]");
if (!settings.DryRun) if (!settings.DryRun)
+61 -1
View File
@@ -1,6 +1,8 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using gregExtractor.GUI.ViewModels; using gregExtractor.GUI.ViewModels;
namespace gregExtractor.GUI; namespace gregExtractor.GUI;
@@ -15,6 +17,9 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
@@ -25,4 +30,59 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted(); 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");
}
}
+23
View File
@@ -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);
}
}
+79 -29
View File
@@ -1,3 +1,4 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using gregExtractor.Models; using gregExtractor.Models;
@@ -6,7 +7,7 @@ using gregExtractor.Utils;
namespace gregExtractor.GUI.ViewModels; namespace gregExtractor.GUI.ViewModels;
public sealed class CoverageViewModel : ObservableObject public sealed class CoverageViewModel : BaseViewModel
{ {
private readonly CoverageAnalyzerService _analyzerService = new(); private readonly CoverageAnalyzerService _analyzerService = new();
@@ -24,12 +25,16 @@ public sealed class CoverageViewModel : ObservableObject
public CoverageViewModel() public CoverageViewModel()
{ {
Log($"[Coverage] Initial FrameworkSourceDirsText: '{_frameworkSourceDirsText}'");
AutoDetectPathCommand = new RelayCommand(AutoDetectPath); AutoDetectPathCommand = new RelayCommand(AutoDetectPath);
BrowseFolderCommand = new AsyncRelayCommand<string?>(BrowseFolderAsync); BrowseFolderCommand = new AsyncRelayCommand<string?>(BrowseFolderAsync);
AnalyzeCoverageCommand = new AsyncRelayCommand(AnalyzeAsync, () => !IsAnalyzing); AnalyzeCoverageCommand = new AsyncRelayCommand(AnalyzeAsync, () => !IsAnalyzing);
OpenMarkdownReportCommand = new RelayCommand(OpenMarkdownReport, () => File.Exists(CoverageAnalyzerService.GetReportPaths(OutputBasePath).MarkdownPath)); OpenMarkdownReportCommand = new RelayCommand(OpenMarkdownReport, () => File.Exists(CoverageAnalyzerService.GetReportPaths(OutputBasePath).MarkdownPath));
AutoDetectPath(); AutoDetectPath();
Log($"[Coverage] After AutoDetect: Il2CppPath='{Il2CppPath}'");
} }
public Func<string?, Task<string?>>? BrowseFolderHandler { get; set; } public Func<string?, Task<string?>>? BrowseFolderHandler { get; set; }
@@ -37,19 +42,19 @@ public sealed class CoverageViewModel : ObservableObject
public string Il2CppPath public string Il2CppPath
{ {
get => _il2CppPath; get => _il2CppPath;
set => SetProperty(ref _il2CppPath, value); set => SetProperty(ref _il2CppPath, value?.Trim() ?? string.Empty);
} }
public string GameHooksPath public string GameHooksPath
{ {
get => _gameHooksPath; get => _gameHooksPath;
set => SetProperty(ref _gameHooksPath, value); set => SetProperty(ref _gameHooksPath, value?.Trim() ?? string.Empty);
} }
public string FrameworkSourceDirsText public string FrameworkSourceDirsText
{ {
get => _frameworkSourceDirsText; get => _frameworkSourceDirsText;
set => SetProperty(ref _frameworkSourceDirsText, value); set => SetProperty(ref _frameworkSourceDirsText, value?.Trim() ?? string.Empty);
} }
public string OutputBasePath public string OutputBasePath
@@ -67,8 +72,16 @@ public sealed class CoverageViewModel : ObservableObject
get => _isAnalyzing; get => _isAnalyzing;
set 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; return;
}
AnalyzeCoverageCommand.NotifyCanExecuteChanged(); AnalyzeCoverageCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsLoading)); OnPropertyChanged(nameof(IsLoading));
@@ -127,10 +140,13 @@ public sealed class CoverageViewModel : ObservableObject
private void AutoDetectPath() private void AutoDetectPath()
{ {
Il2CppPath = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty; string detected = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty;
Log($"[SteamLocator] Found: '{detected}'");
Il2CppPath = detected;
if (string.IsNullOrWhiteSpace(Il2CppPath)) if (string.IsNullOrWhiteSpace(Il2CppPath))
Log("Auto-detect for IL2CPP path failed."); Log("Auto-detect: No IL2CPP path found.");
else else
Log($"Auto-detected IL2CPP path: {Il2CppPath}"); Log($"Auto-detected IL2CPP path: {Il2CppPath}");
} }
@@ -160,61 +176,95 @@ public sealed class CoverageViewModel : ObservableObject
private async Task AnalyzeAsync() 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; return;
} }
if (string.IsNullOrWhiteSpace(GameHooksPath) || !File.Exists(GameHooksPath)) if (string.IsNullOrWhiteSpace(GameHooksPath) || !File.Exists(GameHooksPath))
{ {
Log("game_hooks.json path is invalid."); Log($"game_hooks.json path invalid: '{GameHooksPath}'");
return; return;
} }
Log($"[Coverage] Framework source dirs text: '{FrameworkSourceDirsText}'");
string[] sourceDirs = CoveragePathResolver.ResolveSourceDirs(FrameworkSourceDirsText, Directory.GetCurrentDirectory()); string[] sourceDirs = CoveragePathResolver.ResolveSourceDirs(FrameworkSourceDirsText, Directory.GetCurrentDirectory());
Log($"[Coverage] Resolved {sourceDirs.Length} source dirs: {string.Join(", ", sourceDirs)}");
if (sourceDirs.Length == 0) 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; return;
} }
IsAnalyzing = true; IsAnalyzing = true;
Progress = 0; Progress = 0;
Entries.Clear();
RunOnUIThread(() => Entries.Clear());
try try
{ {
var progress = new Progress<string>(message => var progress = new UIProgress<string>(msg =>
{ {
Log(message); Log(msg);
Progress = Math.Min(99, Progress + 1); Progress = Math.Min(99, Progress + 1);
}); });
CoverageReport report = await Task.Run(() => _analyzerService.Analyze(new AnalyzeOptions( var report = await Task.Run(() => _analyzerService.Analyze(new AnalyzeOptions(
Il2CppDir: Il2CppPath, Il2CppDir: trimmedPath,
GameHooksJsonPath: GameHooksPath, GameHooksJsonPath: GameHooksPath,
FrameworkSourceDirs: sourceDirs, FrameworkSourceDirs: sourceDirs,
OutputBasePath: OutputBasePath, OutputBasePath: OutputBasePath,
Progress: progress))) Progress: progress)))
.ConfigureAwait(false); .ConfigureAwait(false);
TotalHooks = report.TotalHooks; await Dispatcher.UIThread.InvokeAsync(() =>
CoveredCount = report.CoveredCount; {
PlannedCount = report.PlannedCount; TotalHooks = report.TotalHooks;
UncoveredCount = report.UncoveredCount; CoveredCount = report.CoveredCount;
CoveragePercent = report.CoveragePercent; PlannedCount = report.PlannedCount;
UncoveredCount = report.UncoveredCount;
CoveragePercent = report.CoveragePercent;
foreach (HookCoverageEntry entry in report.Entries) foreach (HookCoverageEntry entry in report.Entries)
Entries.Add(entry); Entries.Add(entry);
Progress = 100; Progress = 100;
Log($"Coverage analysis done. Coverage: {report.CoveragePercent:F2}%"); Log($"Coverage analysis done. Coverage: {report.CoveragePercent:F2}%");
OpenMarkdownReportCommand.NotifyCanExecuteChanged(); OpenMarkdownReportCommand.NotifyCanExecuteChanged();
});
} }
catch (Exception exception) catch (Exception ex)
{ {
Log($"Coverage analysis failed: {exception.Message}"); Log($"Coverage analysis failed: {ex.Message}");
} }
finally finally
{ {
@@ -244,4 +294,4 @@ public sealed class CoverageViewModel : ObservableObject
LogLines.Add(line); LogLines.Add(line);
} }
} }
+42 -29
View File
@@ -1,11 +1,13 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using gregExtractor.Models; using gregExtractor.Models;
using gregExtractor.Services; using gregExtractor.Services;
using gregExtractor.Utils;
namespace gregExtractor.GUI.ViewModels; namespace gregExtractor.GUI.ViewModels;
public sealed class CreateViewModel : ObservableObject public sealed class CreateViewModel : BaseViewModel
{ {
private readonly TemplateService _templateService = new(); private readonly TemplateService _templateService = new();
@@ -21,8 +23,7 @@ public sealed class CreateViewModel : ObservableObject
public CreateViewModel() public CreateViewModel()
{ {
string groupsPath = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json"); HookClassifier classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(AddLog));
HookClassifier classifier = HookClassifier.LoadFromFile(groupsPath, new Progress<string>(AddLog));
foreach (string group in classifier.Groups.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) foreach (string group in classifier.Groups.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
Categories.Add(group); Categories.Add(group);
@@ -51,8 +52,16 @@ public sealed class CreateViewModel : ObservableObject
get => _isLoading; get => _isLoading;
set 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; return;
}
NextStepCommand.NotifyCanExecuteChanged(); NextStepCommand.NotifyCanExecuteChanged();
PreviousStepCommand.NotifyCanExecuteChanged(); PreviousStepCommand.NotifyCanExecuteChanged();
@@ -95,7 +104,7 @@ public sealed class CreateViewModel : ObservableObject
get => _outputPath; get => _outputPath;
set set
{ {
if (!SetProperty(ref _outputPath, value)) if (!SetProperty(ref _outputPath, value?.Trim() ?? string.Empty))
return; return;
OpenOutputCommand.NotifyCanExecuteChanged(); OpenOutputCommand.NotifyCanExecuteChanged();
@@ -160,27 +169,28 @@ public sealed class CreateViewModel : ObservableObject
try try
{ {
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json"); string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
TemplateGenerationResult result = await _templateService var result = await Task.Run(() => _templateService.GenerateModAsync(
.GenerateModAsync( ModName,
ModName, SelectedCategory,
SelectedCategory, SelectedTemplateType,
SelectedTemplateType, OutputPath,
OutputPath, hooksPath,
hooksPath, new UIProgress<string>(AddLog),
new Progress<string>(AddLog), CancellationToken.None)).ConfigureAwait(false);
CancellationToken.None)
.ConfigureAwait(false);
PreviewTree.Clear(); await Dispatcher.UIThread.InvokeAsync(() =>
PreviewTree.Add(Path.GetFileName(result.ModDirectory) + "/"); {
foreach (string file in result.GeneratedFiles.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) PreviewTree.Clear();
PreviewTree.Add(" " + Path.GetRelativePath(result.ModDirectory, file).Replace('\\', '/')); 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 finally
{ {
@@ -202,12 +212,15 @@ public sealed class CreateViewModel : ObservableObject
private void RefreshPreview() private void RefreshPreview()
{ {
PreviewTree.Clear(); RunOnUIThread(() =>
PreviewTree.Add(ModName + "/"); {
PreviewTree.Add(" " + ModName + ".csproj"); PreviewTree.Clear();
PreviewTree.Add(" MainPlugin.cs"); PreviewTree.Add(ModName + "/");
PreviewTree.Add(" mod.json"); PreviewTree.Add(" " + ModName + ".csproj");
PreviewTree.Add(" Patches/"); PreviewTree.Add(" MainPlugin.cs");
PreviewTree.Add(" mod.json");
PreviewTree.Add(" Patches/");
});
} }
private void AddLog(string message) private void AddLog(string message)
@@ -218,4 +231,4 @@ public sealed class CreateViewModel : ObservableObject
LogLines.Add(line); LogLines.Add(line);
} }
} }
+46 -20
View File
@@ -1,3 +1,4 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using gregExtractor.Models; using gregExtractor.Models;
@@ -6,7 +7,7 @@ using gregExtractor.Utils;
namespace gregExtractor.GUI.ViewModels; namespace gregExtractor.GUI.ViewModels;
public sealed class ExtractorViewModel : ObservableObject public sealed class ExtractorViewModel : BaseViewModel
{ {
private readonly ExtractorService _extractorService = new(); private readonly ExtractorService _extractorService = new();
private HookClassifier _classifier; private HookClassifier _classifier;
@@ -18,8 +19,7 @@ public sealed class ExtractorViewModel : ObservableObject
public ExtractorViewModel() public ExtractorViewModel()
{ {
string groupsPath = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json"); _classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(AddLog));
_classifier = HookClassifier.LoadFromFile(groupsPath, new Progress<string>(AddLog));
foreach ((string groupName, HookGroupConfig config) in _classifier.Groups) foreach ((string groupName, HookGroupConfig config) in _classifier.Groups)
HookGroups.Add(new HookGroupViewModel(groupName, config.Description)); HookGroups.Add(new HookGroupViewModel(groupName, config.Description));
@@ -50,8 +50,19 @@ public sealed class ExtractorViewModel : ObservableObject
get => _isLoading; get => _isLoading;
set 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; return;
}
StartAnalysisCommand.NotifyCanExecuteChanged(); StartAnalysisCommand.NotifyCanExecuteChanged();
} }
@@ -87,9 +98,9 @@ public sealed class ExtractorViewModel : ObservableObject
Il2CppPath = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty; Il2CppPath = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty;
if (string.IsNullOrWhiteSpace(Il2CppPath)) if (string.IsNullOrWhiteSpace(Il2CppPath))
AddLog("Kein IL2CPP-Pfad automatisch erkannt."); AddLog("Kein IL2CPP-Pfad automatisch erkannt.");
else else
AddLog($"IL2CPP-Pfad erkannt: {Il2CppPath}"); AddLog($"IL2CPP-Pfad erkannt: {Il2CppPath}");
} }
private async Task BrowsePathAsync(string? target) private async Task BrowsePathAsync(string? target)
@@ -111,25 +122,41 @@ public sealed class ExtractorViewModel : ObservableObject
{ {
if (string.IsNullOrWhiteSpace(Il2CppPath) || !Directory.Exists(Il2CppPath)) if (string.IsNullOrWhiteSpace(Il2CppPath) || !Directory.Exists(Il2CppPath))
{ {
AddLog("IL2CPP-Pfad ungültig."); AddLog("IL2CPP-Pfad ungueltig.");
return; return;
} }
IsLoading = true; IsLoading = true;
Progress = 0; Progress = 0;
var progress = new Progress<string>(message => var progress = new UIProgress<string>(msg =>
{ {
AddLog(message); AddLog(msg);
Progress = Math.Min(98, Progress + 1); Progress = Math.Min(98, Progress + 1);
}); });
try try
{ {
IReadOnlyList<HookDefinition> hooks = await _extractorService var result = await _extractorService
.ExtractAsync(Il2CppPath, _classifier, progress, CancellationToken.None) .ExtractAsync(Il2CppPath, _classifier, progress, CancellationToken.None)
.ConfigureAwait(false); .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"); string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
await File.WriteAllTextAsync(hooksPath, JsonSerializer.Serialize(hooks, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None) await File.WriteAllTextAsync(hooksPath, JsonSerializer.Serialize(hooks, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -139,22 +166,21 @@ public sealed class ExtractorViewModel : ObservableObject
await File.WriteAllTextAsync(unknownPath, JsonSerializer.Serialize(unknown, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None) await File.WriteAllTextAsync(unknownPath, JsonSerializer.Serialize(unknown, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
ApplyGroups(hooks); await Dispatcher.UIThread.InvokeAsync(() => ApplyGroups(hooks));
Progress = 100; Progress = 100;
AddLog($"Analyse abgeschlossen. Hooks: {hooks.Count}"); AddLog($"Analyse abgeschlossen. Hooks: {hooks.Count}");
}
catch (Exception exception)
{
AddLog($"✗ Analyse fehlgeschlagen: {exception.Message}");
}
finally
{
IsLoading = false; IsLoading = false;
OnPropertyChanged(nameof(TotalHooks)); OnPropertyChanged(nameof(TotalHooks));
OnPropertyChanged(nameof(GroupedHooks)); OnPropertyChanged(nameof(GroupedHooks));
OnPropertyChanged(nameof(UnknownHooks)); OnPropertyChanged(nameof(UnknownHooks));
OnPropertyChanged(nameof(GroupCount)); OnPropertyChanged(nameof(GroupCount));
} }
catch (Exception ex)
{
AddLog($"Kritischer Fehler: {ex.Message}");
IsLoading = false;
}
} }
private void ApplyGroups(IEnumerable<HookDefinition> hooks) private void ApplyGroups(IEnumerable<HookDefinition> hooks)
@@ -188,4 +214,4 @@ public sealed class ExtractorViewModel : ObservableObject
LogLines.Add(line); LogLines.Add(line);
} }
} }
+59 -28
View File
@@ -1,6 +1,9 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using gregExtractor.Models; using gregExtractor.Models;
using gregExtractor.Services;
using gregExtractor.Utils;
namespace gregExtractor.GUI.ViewModels; namespace gregExtractor.GUI.ViewModels;
@@ -15,14 +18,19 @@ public sealed class HookBrowserRow
public required string Parameters { get; init; } 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 _searchText = string.Empty;
private string _selectedGroup = "Alle"; private string _selectedGroup = "Alle";
private bool _isLoading; private bool _isLoading;
public HookBrowserViewModel() public HookBrowserViewModel()
{ {
_classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(_ => { }));
Groups.Add("Alle"); Groups.Add("Alle");
RefreshCommand = new AsyncRelayCommand(RefreshAsync, () => !IsLoading); RefreshCommand = new AsyncRelayCommand(RefreshAsync, () => !IsLoading);
} }
@@ -35,7 +43,7 @@ public sealed class HookBrowserViewModel : ObservableObject
if (!SetProperty(ref _searchText, value)) if (!SetProperty(ref _searchText, value))
return; return;
ApplyFilter(); RunOnUIThread(ApplyFilter);
} }
} }
@@ -47,7 +55,7 @@ public sealed class HookBrowserViewModel : ObservableObject
if (!SetProperty(ref _selectedGroup, value)) if (!SetProperty(ref _selectedGroup, value))
return; return;
ApplyFilter(); RunOnUIThread(ApplyFilter);
} }
} }
@@ -73,42 +81,65 @@ public sealed class HookBrowserViewModel : ObservableObject
public async Task RefreshAsync() public async Task RefreshAsync()
{ {
IsLoading = true; _classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(_ => { }));
try
string? il2CppPath = SteamLocator.FindIl2CppAssembliesPath();
if (string.IsNullOrWhiteSpace(il2CppPath))
{ {
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json"); RunOnUIThread(() =>
if (!File.Exists(hooksPath))
{ {
Rows.Clear(); Rows.Clear();
FilteredRows.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; return;
} }
string json = await File.ReadAllTextAsync(hooksPath, CancellationToken.None).ConfigureAwait(false); var hooks = result.Hooks;
HookDefinition[] hooks = JsonSerializer.Deserialize<HookDefinition[]>(json) ?? Array.Empty<HookDefinition>();
Rows.Clear(); await RunOnUIThreadAsync(() =>
foreach (HookDefinition hook in hooks)
{ {
Rows.Add(new HookBrowserRow Rows.Clear();
foreach (HookDefinition hook in hooks)
{ {
Group = hook.Group, Rows.Add(new HookBrowserRow
Namespace = hook.Namespace, {
ClassName = hook.ClassName, Group = hook.Group,
MethodName = hook.MethodName, Namespace = hook.Namespace,
ReturnType = hook.ReturnType, ClassName = hook.ClassName,
IsVoid = hook.IsVoid, MethodName = hook.MethodName,
Parameters = string.Join(", ", hook.Parameters.Select(p => $"{p.Type} {p.Name}")), 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); HashSet<string> groups = hooks.Select(h => h.Group).ToHashSet(StringComparer.OrdinalIgnoreCase);
Groups.Clear(); Groups.Clear();
Groups.Add("Alle"); Groups.Add("Alle");
foreach (string group in groups.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) foreach (string group in groups.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
Groups.Add(group); Groups.Add(group);
ApplyFilter(); ApplyFilter();
});
} }
finally finally
{ {
@@ -136,4 +167,4 @@ public sealed class HookBrowserViewModel : ObservableObject
foreach (HookBrowserRow row in query) foreach (HookBrowserRow row in query)
FilteredRows.Add(row); FilteredRows.Add(row);
} }
} }
+1 -1
View File
@@ -3,7 +3,7 @@ using CommunityToolkit.Mvvm.Input;
namespace gregExtractor.GUI.ViewModels; namespace gregExtractor.GUI.ViewModels;
public sealed class MainViewModel : ObservableObject public sealed class MainViewModel : BaseViewModel
{ {
private NavigationItemViewModel? _selectedNavigationItem; private NavigationItemViewModel? _selectedNavigationItem;
private object? _currentPageViewModel; private object? _currentPageViewModel;
+5 -5
View File
@@ -4,7 +4,7 @@ using gregExtractor.Utils;
namespace gregExtractor.GUI.ViewModels; namespace gregExtractor.GUI.ViewModels;
public sealed class SettingsViewModel : ObservableObject public sealed class SettingsViewModel : BaseViewModel
{ {
private string _gamePath = SteamLocator.FindGamePath() ?? string.Empty; private string _gamePath = SteamLocator.FindGamePath() ?? string.Empty;
private string _frameworkSourcePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "gregCore", "src"); private string _frameworkSourcePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "gregCore", "src");
@@ -31,19 +31,19 @@ public sealed class SettingsViewModel : ObservableObject
public string GamePath public string GamePath
{ {
get => _gamePath; get => _gamePath;
set => SetProperty(ref _gamePath, value); set => SetProperty(ref _gamePath, value?.Trim() ?? string.Empty);
} }
public string FrameworkSourcePath public string FrameworkSourcePath
{ {
get => _frameworkSourcePath; get => _frameworkSourcePath;
set => SetProperty(ref _frameworkSourcePath, value); set => SetProperty(ref _frameworkSourcePath, value?.Trim() ?? string.Empty);
} }
public string ModsOutputPath public string ModsOutputPath
{ {
get => _modsOutputPath; get => _modsOutputPath;
set => SetProperty(ref _modsOutputPath, value); set => SetProperty(ref _modsOutputPath, value?.Trim() ?? string.Empty);
} }
public bool IgnoreGettersSetters public bool IgnoreGettersSetters
@@ -146,4 +146,4 @@ public sealed class SettingsViewModel : ObservableObject
{ {
} }
} }
} }
+66 -36
View File
@@ -1,3 +1,4 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using gregExtractor.Models; using gregExtractor.Models;
@@ -23,7 +24,7 @@ public sealed record HookDiffEntry(
public string Display => $"{ClassName}::{MethodName}"; public string Display => $"{ClassName}::{MethodName}";
} }
public sealed class SyncViewModel : ObservableObject public sealed class SyncViewModel : BaseViewModel
{ {
private readonly ExtractorService _extractorService = new(); private readonly ExtractorService _extractorService = new();
private readonly DiffService _diffService = new(); private readonly DiffService _diffService = new();
@@ -51,7 +52,7 @@ public sealed class SyncViewModel : ObservableObject
public string FrameworkSourcePath public string FrameworkSourcePath
{ {
get => _frameworkSourcePath; get => _frameworkSourcePath;
set => SetProperty(ref _frameworkSourcePath, value); set => SetProperty(ref _frameworkSourcePath, value?.Trim() ?? string.Empty);
} }
public bool IsDryRun public bool IsDryRun
@@ -77,12 +78,25 @@ public sealed class SyncViewModel : ObservableObject
get => _isRunning; get => _isRunning;
set set
{ {
if (!SetProperty(ref _isRunning, value)) if (Dispatcher.UIThread.CheckAccess())
return; {
if (!SetProperty(ref _isRunning, value))
return;
CalculateDiffCommand.NotifyCanExecuteChanged(); CalculateDiffCommand.NotifyCanExecuteChanged();
RunSyncCommand.NotifyCanExecuteChanged(); RunSyncCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsLoading)); 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)) if (!Directory.Exists(FrameworkSourcePath))
{ {
AddLog("Framework-Source-Pfad ist ungültig."); AddLog("Framework-Source-Pfad ist ungueltig.");
return; return;
} }
@@ -131,44 +145,49 @@ public sealed class SyncViewModel : ObservableObject
if (diff.Added.Count == 0 && diff.Changed.Count == 0 && diff.Removed.Count == 0) 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; return;
} }
IsRunning = true; IsRunning = true;
Warnings.Clear();
RunOnUIThread(() => Warnings.Clear());
try try
{ {
HookDefinition[] allHooks = await LoadNewHooksAsync(CancellationToken.None).ConfigureAwait(false); 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, FrameworkSourceDir: FrameworkSourcePath,
Diff: diff, Diff: diff,
AllHooks: allHooks.ToList(), AllHooks: allHooks.ToList(),
DryRun: IsDryRun, DryRun: IsDryRun,
Progress: progress)); Progress: progress)), CancellationToken.None).ConfigureAwait(false);
LastResult = result; 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) if (!IsDryRun)
{ {
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json"); string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
await File.WriteAllTextAsync(hooksPath, JsonSerializer.Serialize(allHooks, new JsonSerializerOptions { WriteIndented = true }), CancellationToken.None).ConfigureAwait(false); 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) if (UseGitDiff)
await AppendGitDiffStatAsync(CancellationToken.None).ConfigureAwait(false); await AppendGitDiffStatAsync(CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception exception) catch (Exception ex)
{ {
AddLog($"Sync fehlgeschlagen: {exception.Message}"); AddLog($"Sync fehlgeschlagen: {ex.Message}");
} }
finally finally
{ {
@@ -179,8 +198,12 @@ public sealed class SyncViewModel : ObservableObject
private async Task<HookDiff> ComputeDiffAsync(CancellationToken cancellationToken) private async Task<HookDiff> ComputeDiffAsync(CancellationToken cancellationToken)
{ {
IsRunning = true; IsRunning = true;
DiffEntries.Clear();
Warnings.Clear(); RunOnUIThread(() =>
{
DiffEntries.Clear();
Warnings.Clear();
});
try try
{ {
@@ -190,21 +213,24 @@ public sealed class SyncViewModel : ObservableObject
HookDiff diff = _diffService.Diff(oldHooks.ToList(), newHooks.ToList()); HookDiff diff = _diffService.Diff(oldHooks.ToList(), newHooks.ToList());
_lastDiff = diff; _lastDiff = diff;
foreach (HookDefinition hook in diff.Added) await Dispatcher.UIThread.InvokeAsync(() =>
DiffEntries.Add(ToEntry(HookDiffKind.Added, hook)); {
foreach (HookDefinition hook in diff.Added)
DiffEntries.Add(ToEntry(HookDiffKind.Added, hook));
foreach (HookDefinition hook in diff.Changed) foreach (HookDefinition hook in diff.Changed)
DiffEntries.Add(ToEntry(HookDiffKind.Changed, hook)); DiffEntries.Add(ToEntry(HookDiffKind.Changed, hook));
foreach (HookDefinition hook in diff.Removed) foreach (HookDefinition hook in diff.Removed)
DiffEntries.Add(ToEntry(HookDiffKind.Removed, hook)); DiffEntries.Add(ToEntry(HookDiffKind.Removed, hook));
});
AddLog($"Diff berechnet: +{diff.Added.Count} ~{diff.Changed.Count} -{diff.Removed.Count}"); AddLog($"Diff berechnet: +{diff.Added.Count} ~{diff.Changed.Count} -{diff.Removed.Count}");
return diff; 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>()); return new HookDiff(Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>());
} }
finally finally
@@ -225,18 +251,22 @@ public sealed class SyncViewModel : ObservableObject
private async Task<HookDefinition[]> LoadNewHooksAsync(CancellationToken cancellationToken) private async Task<HookDefinition[]> LoadNewHooksAsync(CancellationToken cancellationToken)
{ {
string groupsPath = Path.Combine(Directory.GetCurrentDirectory(), "hook_groups.json"); HookClassifier classifier = HookClassifier.LoadFromFile(null, new UIProgress<string>(AddLog));
HookClassifier classifier = HookClassifier.LoadFromFile(groupsPath, new Progress<string>(AddLog));
string? il2CppPath = SteamLocator.FindIl2CppAssembliesPath(); string? il2CppPath = SteamLocator.FindIl2CppAssembliesPath();
if (string.IsNullOrWhiteSpace(il2CppPath) || !Directory.Exists(il2CppPath)) if (string.IsNullOrWhiteSpace(il2CppPath) || !Directory.Exists(il2CppPath))
throw new DirectoryNotFoundException("IL2CPP-Pfad konnte nicht automatisch gefunden werden."); throw new DirectoryNotFoundException("IL2CPP-Pfad konnte nicht automatisch gefunden werden.");
IReadOnlyList<HookDefinition> hooks = await _extractorService il2CppPath = il2CppPath.Trim();
.ExtractAsync(il2CppPath, classifier, new Progress<string>(AddLog), cancellationToken)
var result = await _extractorService
.ExtractAsync(il2CppPath, classifier, new UIProgress<string>(AddLog), cancellationToken)
.ConfigureAwait(false); .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) private async Task AppendGitDiffStatAsync(CancellationToken cancellationToken)
@@ -264,7 +294,7 @@ public sealed class SyncViewModel : ObservableObject
AddLog(output.Trim()); AddLog(output.Trim());
if (process.ExitCode != 0 && !string.IsNullOrWhiteSpace(error)) if (process.ExitCode != 0 && !string.IsNullOrWhiteSpace(error))
AddLog("⚠ " + error.Trim()); AddLog(error.Trim());
} }
private void OpenBackupFolder() private void OpenBackupFolder()
@@ -293,4 +323,4 @@ public sealed class SyncViewModel : ObservableObject
SyncLog.Add(line); SyncLog.Add(line);
} }
} }
+18
View File
@@ -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
View File
@@ -1,140 +1,230 @@
using gregExtractor.Models; using gregExtractor.Models;
using Mono.Cecil;
namespace gregExtractor.Services; namespace gregExtractor.Services;
public sealed class ExtractorService public sealed class ExtractorService
{ {
public async Task<IReadOnlyList<HookDefinition>> ExtractAsync( public async Task<ExtractorResult> ExtractAsync(
string il2CppDir, string il2CppDir,
HookClassifier classifier, HookClassifier classifier,
IProgress<string>? progress, IProgress<string>? progress = null,
CancellationToken cancellationToken) CancellationToken ct = default)
{ {
if (string.IsNullOrWhiteSpace(il2CppDir) || !Directory.Exists(il2CppDir)) return await Task.Run(() =>
throw new DirectoryNotFoundException($"IL2CPP directory not found: {il2CppDir}"); {
try
{
ct.ThrowIfCancellationRequested();
string assemblyPath = Path.Combine(il2CppDir, "Assembly-CSharp.dll"); if (string.IsNullOrWhiteSpace(il2CppDir) || !Directory.Exists(il2CppDir))
if (!File.Exists(assemblyPath)) return ExtractorResult.Failure($"IL2CPP directory not found: {il2CppDir}");
throw new FileNotFoundException("Assembly-CSharp.dll was not found.", assemblyPath);
return await Task.Run(() => ExtractInternal(assemblyPath, il2CppDir, classifier, progress, cancellationToken), cancellationToken) var assemblyPath = Path.Combine(il2CppDir, "Assembly-CSharp.dll");
.ConfigureAwait(false); 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 assemblyPath,
string il2CppDir, string il2CppDir,
HookClassifier classifier, IProgress<string>? progress)
IProgress<string>? progress,
CancellationToken cancellationToken)
{ {
var resolver = new DefaultAssemblyResolver(); var resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(il2CppDir); resolver.AddSearchDirectory(il2CppDir);
foreach (string directory in Directory.EnumerateDirectories(il2CppDir)) string? melonDir = Path.GetDirectoryName(il2CppDir);
resolver.AddSearchDirectory(directory); 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, AssemblyResolver = resolver,
ReadingMode = ReadingMode.Deferred, ReadingMode = ReadingMode.Deferred,
ReadSymbols = false, 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); try
var hooks = new List<HookDefinition>(capacity: 8192);
foreach (TypeDefinition type in assembly.MainModule.Types)
{ {
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) private static AssemblyDefinition? LoadAssemblyWithoutResolver(
continue; 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; private static List<HookDefinition> ScanTypesDefensive(
string namespaceName = type.Namespace; 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) foreach (var method in type.Methods)
continue; {
try
{
if (!IsValidMethod(method)) continue;
if (method.IsConstructor || method.IsSpecialName) var hook = BuildHookDefinition(type, method, classifier);
continue; results.Add(hook);
}
if (!method.Parameters.Any() && method.ReturnType.FullName.Equals("System.Void", StringComparison.Ordinal)) catch (Exception ex)
continue; {
progress?.Report($"⚠ Methode übersprungen: {type.Name}.{method?.Name} — {ex.Message}");
var parameters = method.Parameters }
.Select(parameter => new HookParameter(parameter.Name, NormalizeTypeName(parameter.ParameterType))) }
.ToArray(); }
catch (Exception ex)
string returnType = NormalizeTypeName(method.ReturnType); {
bool isVoid = string.Equals(returnType, "void", StringComparison.OrdinalIgnoreCase); progress?.Report($"⚠ Typ übersprungen: {type?.Name} — {ex.Message}");
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));
} }
} }
progress?.Report($"Extraction finished. Hooks found: {hooks.Count}"); return results.OrderBy(h => h.Group, StringComparer.OrdinalIgnoreCase)
.ThenBy(h => h.ClassName, StringComparer.OrdinalIgnoreCase)
return hooks .ThenBy(h => h.MethodName, StringComparer.OrdinalIgnoreCase)
.OrderBy(hook => hook.Group, StringComparer.OrdinalIgnoreCase) .ToList();
.ThenBy(hook => hook.ClassName, StringComparer.OrdinalIgnoreCase)
.ThenBy(hook => hook.MethodName, StringComparer.OrdinalIgnoreCase)
.ToArray();
} }
private static string NormalizeTypeName(TypeReference typeReference) private static HookDefinition BuildHookDefinition(
TypeDefinition type,
MethodDefinition method,
HookClassifier classifier)
{ {
if (typeReference.IsByReference && typeReference is ByReferenceType byReferenceType) var parameters = new List<HookParameter>();
return NormalizeTypeName(byReferenceType.ElementType);
if (typeReference is GenericInstanceType genericInstanceType) foreach (var param in method.Parameters)
{ {
string genericName = genericInstanceType.Name; string paramType = param.ParameterType?.Name ?? "object";
int backtick = genericName.IndexOf('`'); string paramName = string.IsNullOrWhiteSpace(param.Name)
if (backtick >= 0) ? $"arg{param.Index}"
genericName = genericName[..backtick]; : param.Name;
parameters.Add(new HookParameter(paramName, paramType));
string genericArgs = string.Join(", ", genericInstanceType.GenericArguments.Select(NormalizeTypeName));
return $"{genericName}<{genericArgs}>";
} }
if (typeReference.IsArray && typeReference is ArrayType arrayType) string returnTypeName = method.ReturnType?.Name ?? "object";
return $"{NormalizeTypeName(arrayType.ElementType)}[]"; string group = classifier.Classify(type.Name, method.Name);
string ns = type.Namespace ?? string.Empty;
return typeReference.FullName switch return new HookDefinition(
{ Group: group,
"System.Void" => "void", Namespace: ns,
"System.Boolean" => "bool", ClassName: type.Name,
"System.Byte" => "byte", MethodName: method.Name,
"System.SByte" => "sbyte", ReturnType: returnTypeName,
"System.Int16" => "short", IsVoid: string.Equals(returnTypeName, "Void", StringComparison.OrdinalIgnoreCase),
"System.UInt16" => "ushort", Parameters: parameters);
"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,
};
} }
}
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
View File
@@ -6,22 +6,50 @@ public sealed class HookClassifier
{ {
private static readonly Dictionary<string, string[]> MethodKeywordMap = new(StringComparer.OrdinalIgnoreCase) private static readonly Dictionary<string, string[]> MethodKeywordMap = new(StringComparer.OrdinalIgnoreCase)
{ {
["Persistence"] = ["save", "load", "autosave"], ["Persistence"] = ["save", "load", "autosave", "serialize", "deserialize", "generateid"],
["Economy"] = ["coin", "money", "shop", "buy", "xp", "reputation", "checkout"], ["Economy"] = ["coin", "money", "shop", "buy", "xp", "reputation", "checkout", "salary", "revenue", "balance", "app", "customer"],
["Networking"] = ["connect", "device", "cable", "ip", "network", "lacp"], ["Networking"] = ["connect", "device", "cable", "ip", "network", "lacp", "sfp"],
["Hardware"] = ["power", "repair", "broken", "server", "switch"], ["Hardware"] = ["power", "repair", "broken", "server", "switch", "technician", "sfpmodule", "insertport", "slideport"],
["Lifecycle"] = ["pause", "resume", "day", "scene", "quit", "loadlevel"], ["Lifecycle"] = ["pause", "resume", "day", "scene", "quit", "loadlevel", "objective", "operator", "spawn", "destroy"],
["VisualUI"] = ["ui", "menu", "panel", "screen", "button", "hud"], ["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) private static readonly Dictionary<string, string[]> ClassKeywordMap = new(StringComparer.OrdinalIgnoreCase)
{ {
["Persistence"] = ["save"], ["Persistence"] = ["save", "serial", "fcp"],
["Economy"] = ["player", "shop", "economy"], ["Economy"] = ["player", "shop", "economy", "balance", "customer", "bill"],
["Networking"] = ["network", "cable", "packet", "switch", "server"], ["Networking"] = ["network", "cable", "packet", "switch", "server"],
["Hardware"] = ["server", "rack", "item", "asset"], ["Hardware"] = ["server", "rack", "item", "asset", "sfpbox", "setip", "technician", "sfpmodule"],
["Lifecycle"] = ["loading", "pause", "time", "scene", "playermanager"], ["Lifecycle"] = ["loading", "pause", "time", "scene", "playermanager", "objective", "commandcenter", "operator"],
["VisualUI"] = ["menu", "ui", "hud", "panel"], ["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) public HookClassifier(IReadOnlyDictionary<string, HookGroupConfig> groups)
@@ -31,14 +59,18 @@ public sealed class HookClassifier
public IReadOnlyDictionary<string, HookGroupConfig> Groups { get; } 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)); return new HookClassifier(new Dictionary<string, HookGroupConfig>(StringComparer.OrdinalIgnoreCase));
} }
progress?.Report($"Lade hook_groups.json von: {jsonPath}");
try try
{ {
string json = File.ReadAllText(jsonPath); 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) public string Classify(string className, string methodName)
{ {
// 1) Exact class + method match // 1) Exact class + method match
+19
View File
@@ -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
View File
@@ -11,13 +11,13 @@
}, },
"Facility": { "Facility": {
"description": "Building layouts, room expansions, floor designs", "description": "Building layouts, room expansions, floor designs",
"classes": ["RoomManager", "BuildingManager", "FacilityManager", "FloorManager"], "classes": ["RoomManager", "BuildingManager", "FacilityManager", "FloorManager", "Wall", "PushTrolleyHandle", "TrolleyTrigger", "GateLever"],
"methods": ["AddRoom", "RemoveRoom", "ExpandRoom", "PlaceObject", "RemoveObject", "UnlockArea"] "methods": ["AddRoom", "RemoveRoom", "ExpandRoom", "PlaceObject", "RemoveObject", "UnlockArea", "OpenWall", "OnHoverOver", "ObjectAdded", "OpenGate", "CloseGate", "TruckComing"]
}, },
"Hardware": { "Hardware": {
"description": "Custom servers, racks, switches, equipment", "description": "Custom servers, racks, switches, equipment",
"classes": ["Server", "Rack", "Switch", "Item", "ShopItemSO", "AssetManagement"], "classes": ["Server", "Rack", "Switch", "Item", "ShopItemSO", "AssetManagement", "TechnicianManager", "SFPBox", "SetIP", "InputManager", "Technician", "SFPModule"],
"methods": ["PowerButton", "SetIP", "UpdateCustomer", "UpdateAppID", "IsBroken", "RepairDevice", "BuyNewItem"] "methods": ["PowerButton", "SetIP", "UpdateCustomer", "UpdateAppID", "IsBroken", "RepairDevice", "BuyNewItem", "AddDevice", "RemoveDevice", "Connect", "Disconnect", "AssignJob", "RequestJobDelayed", "RotateTowardsGoal", "SendToContainer", "SetHandIKWeight", "InsertDirectlyIntoPort", "SlideIntoPort"]
}, },
"Networking": { "Networking": {
"description": "Cable routing, packet visualization, network configs", "description": "Cable routing, packet visualization, network configs",
@@ -31,27 +31,97 @@
}, },
"Economy": { "Economy": {
"description": "Player economy, XP, shop transactions", "description": "Player economy, XP, shop transactions",
"classes": ["Player", "ComputerShop"], "classes": ["Player", "ComputerShop", "BalanceSheet", "BalanceSheetRow", "CustomerBase", "CustomerBaseDoor", "CustomerCard", "CustomerColor"],
"methods": ["UpdateCoin", "UpdateXP", "UpdateReputation", "ButtonCheckOut", "UpdateCartTotal"] "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": { "Persistence": {
"description": "Save/Load game state", "description": "Save/Load game state",
"classes": ["SaveSystem", "SaveData"], "classes": ["SaveSystem", "SaveData", "ColorSerializationSurrogate", "FCP_Persistence"],
"methods": ["SaveGame", "LoadGame", "AutoSave", "Load", "SaveGameData", "LoadGameData"] "methods": ["SaveGame", "LoadGame", "AutoSave", "Load", "SaveGameData", "LoadGameData", "GetObjectData", "SetObjectData", "GenerateID"]
}, },
"VisualUI": { "VisualUI": {
"description": "Interface overhauls, themes, visual enhancements", "description": "Interface overhauls, themes, visual enhancements",
"classes": ["MainMenu", "PauseMenu", "UIManager", "HUDManager", "AssetManagement"], "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"] "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": { "Lifecycle": {
"description": "Scene, load, pause, time events", "description": "Scene, load, pause, time events",
"classes": ["LoadingScreen", "PauseMenu", "TimeController", "PlayerManager"], "classes": ["LoadingScreen", "PauseMenu", "TimeController", "PlayerManager", "Objectives", "CommandCenter"],
"methods": ["LoadGameScenes", "LoadLevel", "UnLoadLevel", "Pause", "Resume", "OnApplicationQuit"] "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": { "Misc": {
"description": "Other utilities and catch-all hooks", "description": "Other utilities and catch-all hooks",
"classes": [], "classes": [],
"methods": [] "methods": []
} }
} }