From 63e976a451851fa3e297140ac10ff752fe7b059a Mon Sep 17 00:00:00 2001 From: Brandon Hernandez <142270679+Brandher58@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:16:38 -0700 Subject: [PATCH] bugs? --- .../Services/Downloads/ManifestJobFactory.cs | 110 +++++++++++------- src/LuaToolsGui/ViewModels/FixesViewModel.cs | 25 ++-- 2 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/LuaToolsGui/Services/Downloads/ManifestJobFactory.cs b/src/LuaToolsGui/Services/Downloads/ManifestJobFactory.cs index 0379265..b4561e0 100644 --- a/src/LuaToolsGui/Services/Downloads/ManifestJobFactory.cs +++ b/src/LuaToolsGui/Services/Downloads/ManifestJobFactory.cs @@ -577,9 +577,7 @@ private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string fixId, string fixKey = SafeFixKey(fixId); string fixDir = Path.Combine(installDir, FixManifestDir); - string backupDir = Path.Combine(fixDir, fixKey); string manifestPath = Path.Combine(fixDir, $"{fixKey}.json"); - Directory.CreateDirectory(backupDir); var manifest = new DenuvoFixManifest { @@ -588,7 +586,13 @@ private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string fixId, AppliedAt = DateTimeOffset.UtcNow.ToString("o"), }; + // ── Phase 1: plan every entry (modified vs added) WITHOUT writing anything ──────────────── + // Deciding needs only File.Exists → the manifest is written first, so a busy/locked manifest + // path aborts BEFORE a single game file is touched. Otherwise a manifest write failure (bug + // fix for the old catch{} at the end) left .bak files with no manifest to index them — and + // since IsApplied keys off the manifest on disk, no Revert button ever appeared for them. using var archive = ZipFile.OpenRead(file.FilePath); + var plan = new List<(string RelPath, string DestPath, string? BakRel, ZipArchiveEntry Entry)>(); int failed = 0; foreach (var entry in archive.Entries) { @@ -599,39 +603,66 @@ private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string fixId, // count it, rather than writing wherever it points. if (ResolveInside(installDir, entry.FullName) is not { } dest) { failed++; continue; } + // Back up the original — never clobber a known-good .bak. + // + // Keyed by the FULL relative path, not just the file name: fix archives routinely + // ship the same name in several folders (steam_api64.dll, config.ini), and keying + // by name alone collapsed them onto one .bak. The !File.Exists guard below then + // skipped backing the second one up while still recording it as "modified" + // pointing at the first one's backup — so a revert restored one file with the + // other's contents, or silently restored nothing once the .bak was consumed. + if (File.Exists(dest)) + { + string bakRel = $"{fixKey}/{relPath}.bak"; + plan.Add((relPath, dest, bakRel, entry)); + manifest.Files.Add(new DenuvoFixManifestEntry + { + RelativePath = relPath, + Action = "modified", + BackupPath = bakRel, + }); + } + else + { + plan.Add((relPath, dest, null, entry)); + manifest.Files.Add(new DenuvoFixManifestEntry { RelativePath = relPath, Action = "added" }); + } + } + + // ── Phase 2: persist the manifest as the apply's "commit point" ─────────────────────────── + // If it can't be written the apply STOPS here: nothing was extracted, nothing backed up, + // no orphaned state. From this point on the manifest exists on disk, so even a mid-extract + // failure leaves the Revert button usable (IsApplied = manifest exists) — the same + // recoverability guarantee as the old "write even on partial failure", minus the hole where + // the manifest itself failed to persist. + string manifestJson = System.Text.Json.JsonSerializer.Serialize( + manifest, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); + try + { + Directory.CreateDirectory(fixDir); + File.WriteAllText(manifestPath, manifestJson); + } + catch (Exception ex) + { + toast.Show(Resources.Strings.Fixes_Toast_CouldntApply, ex.Message, error: true); + return new JobResult(false, ex.Message); + } + + // ── Phase 3: apply. Every backup write or extract error counts; the already-written + // manifest keeps whatever DID land recoverable (either finished files, or files whose + // backup exists but whose restore is one retry away — e.g. a locked game dir). + foreach (var (relPath, dest, bakRel, entry) in plan) + { try { - if (File.Exists(dest)) + if (bakRel is { } bak) { - // Back up the original — never clobber a known-good .bak. - // - // Keyed by the FULL relative path, not just the file name: fix archives routinely - // ship the same name in several folders (steam_api64.dll, config.ini), and keying - // by name alone collapsed them onto one .bak. The !File.Exists guard below then - // skipped backing the second one up while still recording it as "modified" - // pointing at the first one's backup — so a revert restored one file with the - // other's contents, or silently restored nothing once the .bak was consumed. - string bakRel = $"{fixKey}/{relPath}.bak"; - string bakAbs = Path.Combine(installDir, FixManifestDir, bakRel); + string bakAbs = Path.Combine(installDir, FixManifestDir, bak); if (!File.Exists(bakAbs)) { Directory.CreateDirectory(Path.GetDirectoryName(bakAbs)!); File.Copy(dest, bakAbs, overwrite: false); } - manifest.Files.Add(new DenuvoFixManifestEntry - { - RelativePath = relPath, - Action = "modified", - BackupPath = bakRel, - }); - } - else - { - manifest.Files.Add(new DenuvoFixManifestEntry - { - RelativePath = relPath, - Action = "added", - }); } Directory.CreateDirectory(Path.GetDirectoryName(dest)!); @@ -640,15 +671,6 @@ private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string fixId, catch { failed++; } } - // Write manifest even on partial failure — the backed-up files still need to be recoverable. - try - { - string json = System.Text.Json.JsonSerializer.Serialize( - manifest, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(manifestPath, json); - } - catch { /* best effort */ } - if (failed > 0) { string err = string.Format(Resources.Strings.Fixes_Toast_PartiallyApplied_Body, failed); @@ -735,12 +757,22 @@ public JobResult RevertDenuvoFix(long appId, string fixId, string gameName) { string? installDir = library.GetInstallDir(appId); if (installDir is null) - return new JobResult(false, string.Format(Resources.Strings.Fixes_Toast_GameNotFound_Body, gameName)); + { + // The factory owns every standard revert toast (done / partial / not-found / no-manifest), + // matching the apply path, so callers must not add their own. + string err = string.Format(Resources.Strings.Fixes_Toast_GameNotFound_Body, gameName); + toast.Show(Resources.Strings.Fixes_Toast_GameNotFound, err, error: true); + return new JobResult(false, err); + } string manifestPath = GetFixManifestPath(installDir, fixId); var manifest = ReadFixManifest(manifestPath); if (manifest is null) - return new JobResult(false, Resources.Strings.Fixes_Revert_NoManifest); + { + string err = Resources.Strings.Fixes_Revert_NoManifest; + toast.Show(Resources.Strings.Fixes_Revert_Failed, err, error: true); + return new JobResult(false, err); + } int restored = 0, deleted = 0, errors = 0; @@ -799,7 +831,7 @@ public JobResult RevertDenuvoFix(long appId, string fixId, string gameName) } catch (Exception ex) { - toast.Show(Resources.Strings.Fixes_Toast_CouldntApply, ex.Message, error: true); + toast.Show(Resources.Strings.Fixes_Revert_Failed, ex.Message, error: true); return new JobResult(false, ex.Message); } } diff --git a/src/LuaToolsGui/ViewModels/FixesViewModel.cs b/src/LuaToolsGui/ViewModels/FixesViewModel.cs index 556d84e..8f4db40 100644 --- a/src/LuaToolsGui/ViewModels/FixesViewModel.cs +++ b/src/LuaToolsGui/ViewModels/FixesViewModel.cs @@ -177,9 +177,19 @@ protected override void OnPageSliced(IReadOnlyList slice) [NotifyPropertyChangedFor(nameof(MyGamesHint))] private bool _myGamesOnly; - public string MyGamesHint => _installedAppIds.Count == 0 - ? Resources.Strings.Fixes_MyGames_NotInstalled - : string.Format(Resources.Strings.Fixes_MyGames_Count, _installedAppIds.Count); + /// How many of the user's installed games actually appear in the fix listing. + public string MyGamesHint + { + get + { + if (_installedAppIds.Count == 0) + return Resources.Strings.Fixes_MyGames_NotInstalled; + + int withFixes = _allGames.Count(g => + long.TryParse(g.AppId, out long id) && _installedAppIds.Contains(id)); + return string.Format(Resources.Strings.Fixes_MyGames_Count, withFixes); + } + } partial void OnMyGamesOnlyChanged(bool value) { @@ -435,13 +445,8 @@ private async Task ConfirmRevert() if (!long.TryParse(game.AppId, out long appId)) return; var result = await Task.Run(() => jobs.RevertDenuvoFix(appId, fix.Id, game.Name)); - if (!result.Ok) - { - toast.Show(Resources.Strings.Fixes_Revert_Failed, result.Message ?? "", error: true); - return; - } - - fix.IsApplied = false; + // The factory shows the toast (done / partial / not-found / no-manifest) — single owner. + if (result.Ok) fix.IsApplied = false; } private (FixItemVm Fix, FixGameCardVm Game)? _pendingRevert;