Files
Marvin 2f61e5dc00 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.
2026-04-20 04:26:06 +02:00

326 lines
11 KiB
C#

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 enum HookDiffKind
{
Added,
Changed,
Removed,
}
public sealed record HookDiffEntry(
HookDiffKind Kind,
string Group,
string ClassName,
string MethodName,
string Signature)
{
public string Display => $"{ClassName}::{MethodName}";
}
public sealed class SyncViewModel : BaseViewModel
{
private readonly ExtractorService _extractorService = new();
private readonly DiffService _diffService = new();
private readonly FrameworkSyncService _syncService = new();
private string _frameworkSourcePath = string.Empty;
private bool _isDryRun = true;
private bool _useGitDiff;
private bool _isRunning;
private SyncResult? _lastResult;
private HookDiff? _lastDiff;
public SyncViewModel()
{
FrameworkSourcePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "gregCore", "src");
BrowseSourcePathCommand = new AsyncRelayCommand(BrowseSourcePathAsync);
CalculateDiffCommand = new AsyncRelayCommand(CalculateDiffAsync, () => !IsRunning);
RunSyncCommand = new AsyncRelayCommand(RunSyncAsync, () => !IsRunning);
OpenBackupFolderCommand = new RelayCommand(OpenBackupFolder, () => Directory.Exists(FrameworkSourcePath));
}
public Func<string?, Task<string?>>? BrowseFolderHandler { get; set; }
public string FrameworkSourcePath
{
get => _frameworkSourcePath;
set => SetProperty(ref _frameworkSourcePath, value?.Trim() ?? string.Empty);
}
public bool IsDryRun
{
get => _isDryRun;
set => SetProperty(ref _isDryRun, value);
}
public bool UseGitDiff
{
get => _useGitDiff;
set => SetProperty(ref _useGitDiff, value);
}
public ObservableCollection<HookDiffEntry> DiffEntries { get; } = new();
public ObservableCollection<string> SyncLog { get; } = new();
public ObservableCollection<string> Warnings { get; } = new();
public bool IsRunning
{
get => _isRunning;
set
{
if (Dispatcher.UIThread.CheckAccess())
{
if (!SetProperty(ref _isRunning, value))
return;
CalculateDiffCommand.NotifyCanExecuteChanged();
RunSyncCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsLoading));
}
else
{
Dispatcher.UIThread.Post(() =>
{
SetProperty(ref _isRunning, value);
CalculateDiffCommand.NotifyCanExecuteChanged();
RunSyncCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsLoading));
});
}
}
}
public bool IsLoading => IsRunning;
public SyncResult? LastResult
{
get => _lastResult;
set => SetProperty(ref _lastResult, value);
}
public IAsyncRelayCommand BrowseSourcePathCommand { get; }
public IAsyncRelayCommand CalculateDiffCommand { get; }
public IAsyncRelayCommand RunSyncCommand { get; }
public IRelayCommand OpenBackupFolderCommand { get; }
private async Task BrowseSourcePathAsync()
{
if (BrowseFolderHandler is null)
return;
string? selected = await BrowseFolderHandler("FrameworkSourcePath").ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(selected))
return;
FrameworkSourcePath = selected;
}
private async Task CalculateDiffAsync()
{
await ComputeDiffAsync(CancellationToken.None).ConfigureAwait(false);
}
private async Task RunSyncAsync()
{
if (!Directory.Exists(FrameworkSourcePath))
{
AddLog("Framework-Source-Pfad ist ungueltig.");
return;
}
HookDiff diff = _lastDiff ?? await ComputeDiffAsync(CancellationToken.None).ConfigureAwait(false);
if (diff.Added.Count == 0 && diff.Changed.Count == 0 && diff.Removed.Count == 0)
{
AddLog("Keine Aenderungen erkannt. Sync uebersprungen.");
return;
}
IsRunning = true;
RunOnUIThread(() => Warnings.Clear());
try
{
HookDefinition[] allHooks = await LoadNewHooksAsync(CancellationToken.None).ConfigureAwait(false);
var progress = new UIProgress<string>(AddLog);
var result = await Task.Run(() => _syncService.Sync(new SyncOptions(
FrameworkSourceDir: FrameworkSourcePath,
Diff: diff,
AllHooks: allHooks.ToList(),
DryRun: IsDryRun,
Progress: progress)), CancellationToken.None).ConfigureAwait(false);
LastResult = result;
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");
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}");
}
if (UseGitDiff)
await AppendGitDiffStatAsync(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
AddLog($"Sync fehlgeschlagen: {ex.Message}");
}
finally
{
IsRunning = false;
}
}
private async Task<HookDiff> ComputeDiffAsync(CancellationToken cancellationToken)
{
IsRunning = true;
RunOnUIThread(() =>
{
DiffEntries.Clear();
Warnings.Clear();
});
try
{
HookDefinition[] oldHooks = await LoadOldHooksAsync(cancellationToken).ConfigureAwait(false);
HookDefinition[] newHooks = await LoadNewHooksAsync(cancellationToken).ConfigureAwait(false);
HookDiff diff = _diffService.Diff(oldHooks.ToList(), newHooks.ToList());
_lastDiff = diff;
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.Removed)
DiffEntries.Add(ToEntry(HookDiffKind.Removed, hook));
});
AddLog($"Diff berechnet: +{diff.Added.Count} ~{diff.Changed.Count} -{diff.Removed.Count}");
return diff;
}
catch (Exception ex)
{
AddLog($"Diff-Berechnung fehlgeschlagen: {ex.Message}");
return new HookDiff(Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>(), Array.Empty<HookDefinition>());
}
finally
{
IsRunning = false;
}
}
private async Task<HookDefinition[]> LoadOldHooksAsync(CancellationToken cancellationToken)
{
string hooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
if (!File.Exists(hooksPath))
return Array.Empty<HookDefinition>();
string json = await File.ReadAllTextAsync(hooksPath, cancellationToken).ConfigureAwait(false);
return JsonSerializer.Deserialize<HookDefinition[]>(json) ?? Array.Empty<HookDefinition>();
}
private async Task<HookDefinition[]> LoadNewHooksAsync(CancellationToken cancellationToken)
{
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.");
il2CppPath = il2CppPath.Trim();
var result = await _extractorService
.ExtractAsync(il2CppPath, classifier, new UIProgress<string>(AddLog), cancellationToken)
.ConfigureAwait(false);
if (!result.IsSuccess)
throw new InvalidOperationException(result.Error ?? "Extraction failed");
return result.Hooks.ToArray();
}
private async Task AppendGitDiffStatAsync(CancellationToken cancellationToken)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = "diff --stat",
WorkingDirectory = FrameworkSourcePath,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
},
};
process.Start();
string output = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
string error = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(output))
AddLog(output.Trim());
if (process.ExitCode != 0 && !string.IsNullOrWhiteSpace(error))
AddLog(error.Trim());
}
private void OpenBackupFolder()
{
if (!Directory.Exists(FrameworkSourcePath))
return;
Process.Start(new ProcessStartInfo
{
FileName = FrameworkSourcePath,
UseShellExecute = true,
});
}
private static HookDiffEntry ToEntry(HookDiffKind kind, HookDefinition hook)
{
string signature = $"{hook.ReturnType} {hook.MethodName}({string.Join(", ", hook.Parameters.Select(p => $"{p.Type} {p.Name}"))})";
return new HookDiffEntry(kind, hook.Group, hook.ClassName, hook.MethodName, signature);
}
private void AddLog(string message)
{
string line = $"[{DateTime.Now:HH:mm:ss}] {message}";
if (SyncLog.Count > 1000)
SyncLog.RemoveAt(0);
SyncLog.Add(line);
}
}