using OmniNX.DesktopInstaller.Models; namespace OmniNX.DesktopInstaller.Services; public sealed class InstallEngine { private readonly DeletionService _deletion = new(); private readonly BackupService _backup = new(); private readonly ExtrasService _extras = new(); public async Task RunAsync( InstallOptions options, IProgress progress, CancellationToken cancellationToken = default) { var driveRoot = PathUtils.NormalizeDriveRoot(options.DriveRoot); var staging = options.StagingPath ?? throw new InvalidOperationException("Staging path is required."); if (!Directory.Exists(staging)) throw new DirectoryNotFoundException($"Staging path not found: {staging}"); Report(progress, $"Install mode: {options.Mode}"); Report(progress, $"Pack variant: {VariantHelper.GetDisplayName(options.Variant)}"); if (options.Mode == InstallMode.Update) { await RunUpdateAsync(driveRoot, staging, options, progress, cancellationToken); } else { await RunCleanAsync(driveRoot, staging, options, progress, cancellationToken); } Report(progress, options.Mode == InstallMode.Update ? "Update completed successfully." : "Installation completed successfully."); } private async Task RunUpdateAsync( string driveRoot, string staging, InstallOptions options, IProgress progress, CancellationToken cancellationToken) { if (options.Variant == OmniNxVariant.Oc) { Report(progress, "HorizonOC: backing up config.ini..."); _backup.BackupHorizonOcConfig(driveRoot, StringProgress(progress)); } Report(progress, "Step 1: Cleanup..."); await Task.Run(() => _deletion.RunUpdateDeletion(driveRoot, StringProgress(progress)), cancellationToken); Report(progress, "Step 2: Copying files..."); await Task.Run(() => CopyPackFiles(staging, driveRoot, options.Variant, progress), cancellationToken); Report(progress, "Applying RAM / Hekate-AMS extras..."); await Task.Run(() => _extras.ApplyExtras(driveRoot, options.Use8GbRam, options.UseHekateAmsPro, StringProgress(progress)), cancellationToken); if (options.Variant == OmniNxVariant.Oc) { Report(progress, "HorizonOC: restoring config.ini..."); await Task.Run(() => { _backup.RestoreHorizonOcConfig(driveRoot, StringProgress(progress)); _backup.CleanupHorizonOcBackup(driveRoot); }, cancellationToken); } } private async Task RunCleanAsync( string driveRoot, string staging, InstallOptions options, IProgress progress, CancellationToken cancellationToken) { Report(progress, "Step 1: Backing up user data..."); await Task.Run(() => _backup.BackupUserData(driveRoot, StringProgress(progress)), cancellationToken); Report(progress, "Step 2: Cleaning old installation..."); await Task.Run(() => _deletion.RunCleanDeletion(driveRoot, StringProgress(progress)), cancellationToken); Report(progress, "Step 3: Restoring user data..."); await Task.Run(() => { _backup.RestoreUserData(driveRoot, StringProgress(progress)); _deletion.RunPostRestoreCleanup(driveRoot); _backup.CleanupBackup(driveRoot); }, cancellationToken); Report(progress, "Step 4: Copying files..."); await Task.Run(() => CopyPackFiles(staging, driveRoot, options.Variant, progress), cancellationToken); Report(progress, "Applying RAM / Hekate-AMS extras..."); await Task.Run(() => _extras.ApplyExtras(driveRoot, options.Use8GbRam, options.UseHekateAmsPro, StringProgress(progress)), cancellationToken); } private static void CopyPackFiles( string staging, string driveRoot, OmniNxVariant variant, IProgress progress) { CopyFolderIfExists(staging, driveRoot, "atmosphere", progress); CopyFolderIfExists(staging, driveRoot, "bootloader", progress); CopyFolderIfExists(staging, driveRoot, "config", progress); CopyFolderIfExists(staging, driveRoot, "switch", progress); CopyFolderIfExists(staging, driveRoot, "themes", progress); CopyFolderIfExists(staging, driveRoot, "warmboot_mariko", progress); CopyFolderIfExists(staging, driveRoot, "hwtest", progress); if (variant == OmniNxVariant.Oc) CopyFolderIfExists(staging, driveRoot, "SaltySD", progress); CopyFolderIfExists(staging, driveRoot, "extras", progress); CopyRootFiles(staging, driveRoot, progress); } private static void CopyFolderIfExists( string staging, string driveRoot, string folder, IProgress progress) { var src = Path.Combine(staging, folder); if (!Directory.Exists(src)) return; var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), folder); Report(progress, $"Copying {folder}/..."); PathUtils.CopyDirectory(src, dst, new Progress<(int current, int total, string file)>(copyProgress => { var (current, total, file) = copyProgress; var pct = total > 0 ? current * 100 / total : 0; progress.Report(new InstallProgressReport { Message = $" {folder}/ {file}", Percent = pct, }); })); } private static void CopyRootFiles( string staging, string driveRoot, IProgress progress) { Report(progress, "Copying root files..."); var skipDirs = new HashSet(StringComparer.OrdinalIgnoreCase) { "atmosphere", "bootloader", "config", "switch", "themes", "warmboot_mariko", "hwtest", "SaltySD", "extras", }; foreach (var file in Directory.GetFiles(staging)) { var name = Path.GetFileName(file); if (name.StartsWith('.')) continue; var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), name); PathUtils.CopyFile(file, dst); Report(progress, $" {name}"); } foreach (var dir in Directory.GetDirectories(staging)) { var name = Path.GetFileName(dir); if (skipDirs.Contains(name)) continue; Report(progress, $"Skipping unexpected directory at pack root: {name}"); } } private static void Report(IProgress progress, string message, bool isError = false) { progress.Report(new InstallProgressReport { Message = message, IsError = isError }); } private static IProgress StringProgress(IProgress progress) => new Progress(msg => Report(progress, msg)); }