Skip to content

Commit cae9d2d

Browse files
committed
3.2.8 — manifest caching: skip per-file validation when patch list unchanged
Adds PatchManifestCache that hashes the raw API JSON response and stores it locally. On subsequent launches, if the server hash matches the cached one the full per-file validation is skipped entirely, eliminating the perpetual-update loop. The cache is saved after a clean check or successful update, and cleared before Verify Game Files so a manual verify always runs fresh.
1 parent 9ab8caf commit cae9d2d

3 files changed

Lines changed: 99 additions & 14 deletions

File tree

Wauncher/Services/UpdateService.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public partial class UpdateService : ObservableObject, IUpdateService
4646
private Patches? _cachedPatches;
4747
private bool _forceValidateAllOnce;
4848
private long _lastInstallProgressTick;
49+
private string? _pendingManifestHash;
4950

5051
[ObservableProperty]
5152
private bool _isExtracting;
@@ -230,6 +231,9 @@ public async Task<bool> ValidateGameFilesAsync(bool fullValidate = false)
230231
{
231232
// "Verify Game Files": always hash every file from scratch (ignore the
232233
// fast-path and any cached result), reporting live progress.
234+
// Clear the cached manifest first so a fresh API fetch drives this check.
235+
PatchManifestCache.Clear();
236+
_pendingManifestHash = null;
233237
currentPatches = await Task.Run(() => PatchManager.ValidatePatches(
234238
validateAll: true,
235239
onProgress: (done, total) => Dispatcher.UIThread.Post(() =>
@@ -299,6 +303,14 @@ await DownloadManager.DownloadPatch(
299303
UpdateStatusSpeed = "";
300304
UpdateProgress = 100;
301305
IsUpdateAvailable = false;
306+
307+
// Patch list is now applied — cache the manifest so the next launch skips validation.
308+
if (_pendingManifestHash != null)
309+
{
310+
PatchManifestCache.Save(_pendingManifestHash);
311+
_pendingManifestHash = null;
312+
}
313+
302314
return true;
303315
}
304316
catch (Exception ex)
@@ -324,13 +336,32 @@ await DownloadManager.DownloadPatch(
324336

325337
try
326338
{
327-
var patches = await PatchManager.ValidatePatches(deleteOutdatedFiles: false);
339+
string rawJson = await PatchManager.FetchPatchJsonAsync();
340+
string manifestHash = PatchManifestCache.ComputeHash(rawJson);
341+
_pendingManifestHash = manifestHash;
342+
343+
// If the server's patch list hasn't changed since the last successful check,
344+
// skip full per-file hashing — any API change (new file, new hash) changes this.
345+
if (PatchManifestCache.Load() == manifestHash)
346+
{
347+
_cachedPatches = new Patches(true, new List<Patch>(), new List<Patch>());
348+
return _cachedPatches;
349+
}
350+
351+
var patches = await Task.Run(() => PatchManager.ValidatePatches(rawJson, deleteOutdatedFiles: false));
328352
_cachedPatches = patches;
353+
354+
// Game already up to date — save manifest so next launch skips validation.
355+
if (patches.Missing.Count == 0 && patches.Outdated.Count == 0)
356+
{
357+
PatchManifestCache.Save(manifestHash);
358+
_pendingManifestHash = null;
359+
}
360+
329361
return patches;
330362
}
331363
catch (UpdateServerUnreachableException ex)
332364
{
333-
// Surface a clear, friendly message in the bottom bar instead of hanging/failing silently.
334365
ErrorLogger.LogError("UpdateService.GetPatchesAsync", ex, "Update server unreachable");
335366
UpdateStatusFile = "Error: Can't connect to update server";
336367
return null;

Wauncher/Utils/Patch.cs

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,46 @@
22
using Newtonsoft.Json.Linq;
33
using System.Collections.Concurrent;
44
using System.Security.Cryptography;
5+
using System.Text;
56
using System.Threading;
67

78
namespace Wauncher.Utils
89
{
10+
internal static class PatchManifestCache
11+
{
12+
private static string CachePath => Path.Combine(
13+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
14+
"ClassicCounter", "Wauncher", "patch_manifest.hash");
15+
16+
public static string? Load()
17+
{
18+
try { return File.ReadAllText(CachePath).Trim(); }
19+
catch { return null; }
20+
}
21+
22+
public static void Save(string hash)
23+
{
24+
try
25+
{
26+
Directory.CreateDirectory(Path.GetDirectoryName(CachePath)!);
27+
File.WriteAllText(CachePath, hash);
28+
}
29+
catch { }
30+
}
31+
32+
public static void Clear()
33+
{
34+
try { File.Delete(CachePath); }
35+
catch { }
36+
}
37+
38+
public static string ComputeHash(string input)
39+
{
40+
var bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
41+
return Convert.ToHexString(bytes).ToLowerInvariant();
42+
}
43+
}
44+
945
public class Patch
1046
{
1147
[JsonProperty(PropertyName = "file")]
@@ -40,23 +76,42 @@ private static string GetOriginalFileName(string fileName)
4076
return fileName.EndsWith(".7z") ? fileName[..^3] : fileName;
4177
}
4278

43-
private static async Task<List<Patch>> GetPatches(bool validateAll = false)
79+
public static async Task<string> FetchPatchJsonAsync()
4480
{
45-
List<Patch> patches = new List<Patch>();
46-
47-
string responseString;
4881
try
4982
{
50-
responseString = await Api.ClassicCounter.GetPatches();
83+
return await Api.ClassicCounter.GetPatches();
5184
}
5285
catch (Exception ex)
5386
{
54-
// Network failure / timeout / DNS — surface it instead of swallowing,
55-
// so the UI can say "Can't connect to update server" rather than hang.
5687
if (Debug.Enabled())
57-
Terminal.Debug($"Couldn't reach {(validateAll ? "full game" : "patch")} API: {ex.Message}");
88+
Terminal.Debug($"Couldn't reach patch API: {ex.Message}");
5889
throw new UpdateServerUnreachableException("Can't connect to update server", ex);
5990
}
91+
}
92+
93+
private static async Task<List<Patch>> GetPatches(string? rawJson = null)
94+
{
95+
List<Patch> patches = new List<Patch>();
96+
97+
string responseString;
98+
if (rawJson != null)
99+
{
100+
responseString = rawJson;
101+
}
102+
else
103+
{
104+
try
105+
{
106+
responseString = await Api.ClassicCounter.GetPatches();
107+
}
108+
catch (Exception ex)
109+
{
110+
if (Debug.Enabled())
111+
Terminal.Debug($"Couldn't reach patch API: {ex.Message}");
112+
throw new UpdateServerUnreachableException("Can't connect to update server", ex);
113+
}
114+
}
60115

61116
try
62117
{
@@ -67,7 +122,6 @@ private static async Task<List<Patch>> GetPatches(bool validateAll = false)
67122
}
68123
catch (Exception ex)
69124
{
70-
// Reachable but returned something we can't parse — treat as a server problem.
71125
if (Debug.Enabled())
72126
Terminal.Debug($"Update server returned invalid data: {ex.Message}");
73127
throw new UpdateServerUnreachableException("Update server returned invalid data", ex);
@@ -84,9 +138,9 @@ private static async Task<string> GetHash(string filePath)
84138
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
85139
}
86140

87-
public static async Task<Patches> ValidatePatches(bool validateAll = false, bool deleteOutdatedFiles = true, Action<int, int>? onProgress = null)
141+
public static async Task<Patches> ValidatePatches(string? rawJson = null, bool validateAll = false, bool deleteOutdatedFiles = true, Action<int, int>? onProgress = null)
88142
{
89-
List<Patch> patches = await GetPatches(validateAll);
143+
List<Patch> patches = await GetPatches(rawJson);
90144
List<Patch> missing = new();
91145
List<Patch> outdated = new();
92146
Patch? dirPatch = null;

Wauncher/Wauncher.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PublishSingleFile>true</PublishSingleFile>
1616
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
1717
<SelfContained>false</SelfContained>
18-
<Version>3.2.7</Version>
18+
<Version>3.2.8</Version>
1919
<AssemblyVersion>$(Version)</AssemblyVersion>
2020
<FileVersion>$(Version)</FileVersion>
2121
<AssemblyName>wauncher</AssemblyName>

0 commit comments

Comments
 (0)