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

297 lines
8.9 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 sealed class CoverageViewModel : BaseViewModel
{
private readonly CoverageAnalyzerService _analyzerService = new();
private string _il2CppPath = string.Empty;
private string _gameHooksPath = Path.Combine(Directory.GetCurrentDirectory(), "game_hooks.json");
private string _frameworkSourceDirsText = CoveragePathResolver.GetDefaultSourcesText(Directory.GetCurrentDirectory());
private string _outputBasePath = Path.Combine(Directory.GetCurrentDirectory(), "coverage_report");
private bool _isAnalyzing;
private double _progress;
private int _totalHooks;
private int _coveredCount;
private int _plannedCount;
private int _uncoveredCount;
private double _coveragePercent;
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; }
public string Il2CppPath
{
get => _il2CppPath;
set => SetProperty(ref _il2CppPath, value?.Trim() ?? string.Empty);
}
public string GameHooksPath
{
get => _gameHooksPath;
set => SetProperty(ref _gameHooksPath, value?.Trim() ?? string.Empty);
}
public string FrameworkSourceDirsText
{
get => _frameworkSourceDirsText;
set => SetProperty(ref _frameworkSourceDirsText, value?.Trim() ?? string.Empty);
}
public string OutputBasePath
{
get => _outputBasePath;
set
{
if (SetProperty(ref _outputBasePath, value))
OpenMarkdownReportCommand.NotifyCanExecuteChanged();
}
}
public bool IsAnalyzing
{
get => _isAnalyzing;
set
{
if (Dispatcher.UIThread.CheckAccess())
{
if (!SetProperty(ref _isAnalyzing, value))
return;
}
else
{
Dispatcher.UIThread.Post(() => SetProperty(ref _isAnalyzing, value));
return;
}
AnalyzeCoverageCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsLoading));
}
}
public bool IsLoading => IsAnalyzing;
public double Progress
{
get => _progress;
set => SetProperty(ref _progress, value);
}
public int TotalHooks
{
get => _totalHooks;
set => SetProperty(ref _totalHooks, value);
}
public int CoveredCount
{
get => _coveredCount;
set => SetProperty(ref _coveredCount, value);
}
public int PlannedCount
{
get => _plannedCount;
set => SetProperty(ref _plannedCount, value);
}
public int UncoveredCount
{
get => _uncoveredCount;
set => SetProperty(ref _uncoveredCount, value);
}
public double CoveragePercent
{
get => _coveragePercent;
set => SetProperty(ref _coveragePercent, value);
}
public ObservableCollection<HookCoverageEntry> Entries { get; } = new();
public ObservableCollection<string> LogLines { get; } = new();
public IRelayCommand AutoDetectPathCommand { get; }
public IAsyncRelayCommand<string?> BrowseFolderCommand { get; }
public IAsyncRelayCommand AnalyzeCoverageCommand { get; }
public IRelayCommand OpenMarkdownReportCommand { get; }
private void AutoDetectPath()
{
string detected = SteamLocator.FindIl2CppAssembliesPath() ?? string.Empty;
Log($"[SteamLocator] Found: '{detected}'");
Il2CppPath = detected;
if (string.IsNullOrWhiteSpace(Il2CppPath))
Log("Auto-detect: No IL2CPP path found.");
else
Log($"Auto-detected IL2CPP path: {Il2CppPath}");
}
private async Task BrowseFolderAsync(string? target)
{
if (BrowseFolderHandler is null)
return;
string? selected = await BrowseFolderHandler(target).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(selected))
return;
switch (target)
{
case "Il2Cpp":
Il2CppPath = selected;
break;
case "SourceDirs":
FrameworkSourceDirsText = selected;
break;
case "Output":
OutputBasePath = Path.Combine(selected, "coverage_report");
break;
}
}
private async Task AnalyzeAsync()
{
Log($"[Coverage] Input IL2CPP path: '{Il2CppPath}'");
string trimmedPath = Il2CppPath?.Trim() ?? string.Empty;
Log($"[Coverage] Trimmed path: '{trimmedPath}'");
if (string.IsNullOrWhiteSpace(trimmedPath))
{
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 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("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;
RunOnUIThread(() => Entries.Clear());
try
{
var progress = new UIProgress<string>(msg =>
{
Log(msg);
Progress = Math.Min(99, Progress + 1);
});
var report = await Task.Run(() => _analyzerService.Analyze(new AnalyzeOptions(
Il2CppDir: trimmedPath,
GameHooksJsonPath: GameHooksPath,
FrameworkSourceDirs: sourceDirs,
OutputBasePath: OutputBasePath,
Progress: progress)))
.ConfigureAwait(false);
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);
Progress = 100;
Log($"Coverage analysis done. Coverage: {report.CoveragePercent:F2}%");
OpenMarkdownReportCommand.NotifyCanExecuteChanged();
});
}
catch (Exception ex)
{
Log($"Coverage analysis failed: {ex.Message}");
}
finally
{
IsAnalyzing = false;
}
}
private void OpenMarkdownReport()
{
string markdownPath = CoverageAnalyzerService.GetReportPaths(OutputBasePath).MarkdownPath;
if (!File.Exists(markdownPath))
return;
Process.Start(new ProcessStartInfo
{
FileName = markdownPath,
UseShellExecute = true,
});
}
private void Log(string message)
{
string line = $"[{DateTime.Now:HH:mm:ss}] {message}";
if (LogLines.Count > 1000)
LogLines.RemoveAt(0);
LogLines.Add(line);
}
}