Skip to content

Commit 08a8844

Browse files
committed
fix(patch): Fix progress update on compute MD5 hash step
1 parent bbf2cec commit 08a8844

3 files changed

Lines changed: 248 additions & 30 deletions

File tree

Hi3Helper.Plugin.Wuwa/Management/WuwaGameInstaller.Patch.cs

Lines changed: 164 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,34 @@ internal async Task<long> CalculatePatchSizeAsync(GameInstallerKind kind, Cancel
8181

8282
ulong total = 0;
8383
int krpCount = 0;
84+
int fullCount = 0;
8485
foreach (var entry in patchIndex.Resource)
8586
{
86-
if (!string.IsNullOrEmpty(entry.Dest) &&
87-
entry.Dest.EndsWith(".krpdiff", StringComparison.OrdinalIgnoreCase))
88-
{
89-
total += entry.Size;
87+
if (string.IsNullOrEmpty(entry.Dest))
88+
continue;
89+
90+
if (entry.Dest.EndsWith(".krpdiff", StringComparison.OrdinalIgnoreCase))
9091
krpCount++;
91-
}
92+
else
93+
fullCount++;
94+
}
95+
96+
// If there are krpdiff entries, sum only those (small diffs).
97+
// If there are none (old-style patch), sum the full-replacement entries instead.
98+
bool hasKrpdiffs = krpCount > 0;
99+
foreach (var entry in patchIndex.Resource)
100+
{
101+
if (string.IsNullOrEmpty(entry.Dest))
102+
continue;
103+
104+
bool isKrpdiff = entry.Dest.EndsWith(".krpdiff", StringComparison.OrdinalIgnoreCase);
105+
if (hasKrpdiffs ? isKrpdiff : !isKrpdiff)
106+
total += entry.Size;
92107
}
93108

94109
SharedStatic.InstanceLogger.LogInformation(
95-
"[WuwaGameInstaller::CalculatePatchSizeAsync] Computed patch size: {Size} bytes from {Count} krpdiff entries (version {Version})",
96-
total, krpCount, currentVersion);
110+
"[WuwaGameInstaller::CalculatePatchSizeAsync] Computed patch size: {Size} bytes — {KrpCount} krpdiff, {FullCount} full-replacement entries (version {Version})",
111+
total, krpCount, fullCount, currentVersion);
97112

98113
return total > long.MaxValue ? long.MaxValue : (long)total;
99114
}
@@ -373,14 +388,26 @@ public async Task RunAsync(
373388

374389
// Initialize progress tracking
375390
var installProgress = new InstallProgress();
391+
var currentProgressState = InstallProgressState.Idle;
376392

377393
void ReportProgress()
378394
{
379-
progressDelegate?.Invoke(in installProgress);
395+
// Build a snapshot so the host/COM layer sees fully-consistent memory
396+
InstallProgress snap = default;
397+
snap.StateCount = Volatile.Read(ref installProgress.StateCount);
398+
snap.TotalStateToComplete = Volatile.Read(ref installProgress.TotalStateToComplete);
399+
snap.DownloadedCount = Volatile.Read(ref installProgress.DownloadedCount);
400+
snap.TotalCountToDownload = Volatile.Read(ref installProgress.TotalCountToDownload);
401+
snap.DownloadedBytes = Interlocked.Read(ref installProgress.DownloadedBytes);
402+
snap.TotalBytesToDownload = Interlocked.Read(ref installProgress.TotalBytesToDownload);
403+
404+
progressDelegate?.Invoke(in snap);
405+
progressStateDelegate?.Invoke(currentProgressState);
380406
}
381407

382408
// ── Step 1: Resolve the correct patch config ──
383-
progressStateDelegate?.Invoke(InstallProgressState.Preparing);
409+
currentProgressState = InstallProgressState.Preparing;
410+
ReportProgress();
384411

385412
manager.GetCurrentGameVersion(out GameVersion currentVersion);
386413

@@ -435,6 +462,42 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
435462
// externally) but all files on disk are already at the target version.
436463
if (!onlyDownload && patchIndex.GroupInfos.Length > 0)
437464
{
465+
currentProgressState = InstallProgressState.Verify;
466+
467+
// Count total file pairs and total bytes (via fast metadata stat)
468+
// for smooth progress during large-file hashing.
469+
int totalPreflightPairs = 0;
470+
long totalPreflightBytes = 0;
471+
foreach (var g in patchIndex.GroupInfos)
472+
{
473+
int pairs = Math.Min(g.SrcFiles.Length, g.DstFiles.Length);
474+
totalPreflightPairs += pairs;
475+
for (int pi = 0; pi < pairs; pi++)
476+
{
477+
var dst = g.DstFiles[pi];
478+
if (string.IsNullOrEmpty(dst.Dest) || string.IsNullOrEmpty(dst.Md5))
479+
continue;
480+
string p = Path.Combine(installPath,
481+
dst.Dest.Replace('/', Path.DirectorySeparatorChar));
482+
if (File.Exists(p))
483+
totalPreflightBytes += new FileInfo(p).Length;
484+
}
485+
}
486+
487+
// State tracks file count (displayed by host as counter text).
488+
// Bytes track hashed data with mid-file granularity for smooth progress.
489+
installProgress.TotalCountToDownload = totalPreflightPairs;
490+
installProgress.DownloadedCount = 0;
491+
installProgress.TotalStateToComplete = totalPreflightPairs;
492+
installProgress.StateCount = 0;
493+
installProgress.TotalBytesToDownload = totalPreflightBytes;
494+
installProgress.DownloadedBytes = 0;
495+
ReportProgress();
496+
497+
SharedStatic.InstanceLogger.LogInformation(
498+
"[Patch::RunAsync] Pre-flight validation: checking {FileCount} files across {GroupCount} groups against target hashes...",
499+
totalPreflightPairs, patchIndex.GroupInfos.Length);
500+
438501
bool allDestinationsMatch = true;
439502
int checkedCount = 0;
440503

@@ -454,6 +517,8 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
454517

455518
if (!File.Exists(dstPath))
456519
{
520+
SharedStatic.InstanceLogger.LogDebug(
521+
"[Patch::RunAsync] Pre-flight: file missing, will patch: {File}", dstRef.Dest);
457522
allDestinationsMatch = false;
458523
break;
459524
}
@@ -462,22 +527,48 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
462527
var fi = new FileInfo(dstPath);
463528
if (dstRef.Size > 0 && (ulong)fi.Length != dstRef.Size)
464529
{
530+
SharedStatic.InstanceLogger.LogDebug(
531+
"[Patch::RunAsync] Pre-flight: size mismatch for {File} (expected={Expected}, actual={Actual}), will patch.",
532+
dstRef.Dest, dstRef.Size, fi.Length);
465533
allDestinationsMatch = false;
466534
break;
467535
}
468536

469537
// MD5 check
538+
SharedStatic.InstanceLogger.LogDebug(
539+
"[Patch::RunAsync] Pre-flight: hashing {File} ({Size})...",
540+
dstRef.Dest, fi.Length);
541+
470542
await using (var fs = File.OpenRead(dstPath))
471543
{
472-
string md5 = await WuwaUtils.ComputeMd5HexAsync(fs, token).ConfigureAwait(false);
544+
long hashBytesAccum = 0;
545+
const long reportThreshold = 4 << 20; // report every ~4 MiB
546+
547+
string md5 = await WuwaUtils.ComputeMd5HexAsync(fs, bytesRead =>
548+
{
549+
Interlocked.Add(ref installProgress.DownloadedBytes, bytesRead);
550+
hashBytesAccum += bytesRead;
551+
if (hashBytesAccum >= reportThreshold)
552+
{
553+
ReportProgress();
554+
hashBytesAccum = 0;
555+
}
556+
}, token).ConfigureAwait(false);
557+
473558
if (!string.Equals(md5, dstRef.Md5, StringComparison.OrdinalIgnoreCase))
474559
{
560+
SharedStatic.InstanceLogger.LogDebug(
561+
"[Patch::RunAsync] Pre-flight: MD5 mismatch for {File}, will patch.",
562+
dstRef.Dest);
475563
allDestinationsMatch = false;
476564
break;
477565
}
478566
}
479567

480568
checkedCount++;
569+
Interlocked.Increment(ref installProgress.DownloadedCount);
570+
Interlocked.Increment(ref installProgress.StateCount);
571+
ReportProgress();
481572
}
482573

483574
if (!allDestinationsMatch)
@@ -499,6 +590,16 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
499590
manager.GetApiGameVersion(out preflightTargetVer);
500591

501592
manager.SetCurrentGameVersion(preflightTargetVer);
593+
594+
// Clear DEBUG downgrade flags so the spoofed version doesn't
595+
// re-trigger another update cycle on next init/LoadConfig.
596+
if (manager.DEBUG_AllowDowngrade)
597+
{
598+
SharedStatic.InstanceLogger.LogInformation(
599+
"[Patch::RunAsync] Clearing DEBUG_AllowDowngrade after successful pre-flight.");
600+
manager.DEBUG_AllowDowngrade = false;
601+
}
602+
502603
manager.SaveConfig();
503604

504605
// Clean up any leftover preload temp files
@@ -509,7 +610,8 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
509610
}
510611
catch { /* best-effort */ }
511612

512-
progressStateDelegate?.Invoke(InstallProgressState.Completed);
613+
currentProgressState = InstallProgressState.Completed;
614+
ReportProgress();
513615
return;
514616
}
515617

@@ -549,18 +651,39 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
549651
patchTempPath, krpdiffEntries.Length);
550652
}
551653

552-
// ── Step 5: Download krpdiff files (if not pre-downloaded) ──
553-
if (!hasPredownloadedFiles && krpdiffEntries.Length > 0)
654+
// ── Step 5: Download patch files ──
655+
// If krpdiff entries exist, download only those (small diffs applied via groupInfos).
656+
// If no krpdiff entries exist (old-style patch), download the full replacement entries.
657+
WuwaApiResponseResourceEntry[] downloadEntries;
658+
if (krpdiffEntries.Length > 0)
659+
{
660+
downloadEntries = hasPredownloadedFiles
661+
? [] // already pre-downloaded
662+
: krpdiffEntries;
663+
}
664+
else
665+
{
666+
// Old-style patch: all resources are full replacement files
667+
downloadEntries = patchIndex.Resource
668+
.Where(e => !string.IsNullOrEmpty(e.Dest))
669+
.ToArray();
670+
671+
SharedStatic.InstanceLogger.LogInformation(
672+
"[Patch::RunAsync] No krpdiff entries found — old-style patch. Downloading {Count} full replacement files.",
673+
downloadEntries.Length);
674+
}
675+
676+
if (downloadEntries.Length > 0)
554677
{
555-
progressStateDelegate?.Invoke(InstallProgressState.Download);
678+
currentProgressState = InstallProgressState.Download;
556679

557680
// Calculate total bytes and set progress
558681
ulong totalBytes = 0;
559-
foreach (var e in krpdiffEntries)
682+
foreach (var e in downloadEntries)
560683
totalBytes += e.Size;
561684

562685
installProgress.TotalBytesToDownload = totalBytes > long.MaxValue ? long.MaxValue : (long)totalBytes;
563-
installProgress.TotalCountToDownload = krpdiffEntries.Length;
686+
installProgress.TotalCountToDownload = downloadEntries.Length;
564687
installProgress.DownloadedBytes = 0;
565688
installProgress.DownloadedCount = 0;
566689
ReportProgress();
@@ -574,7 +697,7 @@ await installer.RunAsync(kind, progressDelegate, progressStateDelegate, token)
574697

575698
Directory.CreateDirectory(patchTempPath);
576699

577-
await Parallel.ForEachAsync(krpdiffEntries,
700+
await Parallel.ForEachAsync(downloadEntries,
578701
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = token },
579702
async (entry, ct) =>
580703
{
@@ -617,13 +740,14 @@ await _owner.TryDownloadWholeFileWithFallbacksAsync(
617740
}).ConfigureAwait(false);
618741

619742
SharedStatic.InstanceLogger.LogInformation(
620-
"[Patch::RunAsync] Download phase complete. Downloaded {Count} krpdiff files.",
621-
krpdiffEntries.Length);
743+
"[Patch::RunAsync] Download phase complete. Downloaded {Count} files.",
744+
downloadEntries.Length);
622745
}
623746

624747
// ── Step 6: Verify downloaded files ──
625-
progressStateDelegate?.Invoke(InstallProgressState.Verify);
626-
foreach (var entry in krpdiffEntries)
748+
currentProgressState = InstallProgressState.Verify;
749+
ReportProgress();
750+
foreach (var entry in downloadEntries)
627751
{
628752
token.ThrowIfCancellationRequested();
629753
if (string.IsNullOrEmpty(entry.Dest))
@@ -635,7 +759,7 @@ await _owner.TryDownloadWholeFileWithFallbacksAsync(
635759
if (!File.Exists(filePath))
636760
{
637761
throw new FileNotFoundException(
638-
$"KRPDiff file missing after download: {entry.Dest}", filePath);
762+
$"Patch file missing after download: {entry.Dest}", filePath);
639763
}
640764

641765
var fileInfo = new FileInfo(filePath);
@@ -654,7 +778,7 @@ await _owner.TryDownloadWholeFileWithFallbacksAsync(
654778
if (!string.Equals(computedMd5, entry.Md5, StringComparison.OrdinalIgnoreCase))
655779
{
656780
throw new InvalidOperationException(
657-
$"MD5 mismatch for downloaded krpdiff {entry.Dest}: expected={entry.Md5}, computed={computedMd5}");
781+
$"MD5 mismatch for downloaded file {entry.Dest}: expected={entry.Md5}, computed={computedMd5}");
658782
}
659783
}
660784
}
@@ -670,7 +794,8 @@ await _owner.TryDownloadWholeFileWithFallbacksAsync(
670794
string markerPath = Path.Combine(patchTempPath, ".version");
671795
await File.WriteAllTextAsync(markerPath, targetVersion.ToString(), token).ConfigureAwait(false);
672796

673-
progressStateDelegate?.Invoke(InstallProgressState.Completed);
797+
currentProgressState = InstallProgressState.Completed;
798+
ReportProgress();
674799
SharedStatic.InstanceLogger.LogInformation(
675800
"[Patch::RunAsync] Preload download complete. Files saved to {Path}. Target version: {Version}",
676801
patchTempPath, targetVersion);
@@ -680,7 +805,8 @@ await _owner.TryDownloadWholeFileWithFallbacksAsync(
680805
// ── Step 8: Delete files from deleteFiles list ──
681806
if (patchIndex.DeleteFiles.Length > 0)
682807
{
683-
progressStateDelegate?.Invoke(InstallProgressState.Removing);
808+
currentProgressState = InstallProgressState.Removing;
809+
ReportProgress();
684810
foreach (var deleteEntry in patchIndex.DeleteFiles)
685811
{
686812
if (string.IsNullOrEmpty(deleteEntry.Dest))
@@ -710,7 +836,7 @@ await _owner.TryDownloadWholeFileWithFallbacksAsync(
710836
// ── Step 9: Apply patches from groupInfos ──
711837
if (patchIndex.GroupInfos.Length > 0)
712838
{
713-
progressStateDelegate?.Invoke(InstallProgressState.Updating);
839+
currentProgressState = InstallProgressState.Updating;
714840

715841
// Count total file pairs across all groups for accurate progress tracking
716842
int totalFilePairs = 0;
@@ -907,9 +1033,20 @@ await File.WriteAllTextAsync(progressMarkerPath, currentPairIndex.ToString(), to
9071033
}
9081034

9091035
manager.SetCurrentGameVersion(targetVer);
1036+
1037+
// Clear DEBUG downgrade flags so the spoofed version doesn't re-trigger
1038+
// another update cycle on next init.
1039+
if (manager.DEBUG_AllowDowngrade)
1040+
{
1041+
SharedStatic.InstanceLogger.LogInformation(
1042+
"[Patch::RunAsync] Clearing DEBUG_AllowDowngrade after successful patch.");
1043+
manager.DEBUG_AllowDowngrade = false;
1044+
}
1045+
9101046
manager.SaveConfig();
9111047

912-
progressStateDelegate?.Invoke(InstallProgressState.Completed);
1048+
currentProgressState = InstallProgressState.Completed;
1049+
ReportProgress();
9131050
SharedStatic.InstanceLogger.LogInformation(
9141051
"[Patch::RunAsync] Patch complete. Game updated to version {Version}.", targetVer);
9151052
}

0 commit comments

Comments
 (0)