Skip to content

Commit 41500b5

Browse files
committed
[Plugin] Implement preload download and safe callbacks
Add full async preload download flow and harden progress/status callbacks. - Implement StartPackageDownload as an async method (with NoOptimization/NoInlining) that initializes the plugin COM, queries total and already-downloaded sizes, ensures disk space, starts the preload task and awaits it. Handles cancellation, exceptions, and updates Status/IsRunning and logging. - Implement IsPreloadCompleted to check preload completion by comparing downloaded vs total size via plugin calls (with error handling). - Add defensive try/catch around UpdateProgressCallback and UpdateStatusCallback to prevent unhandled exceptions from crossing reverse P/Invoke boundaries (which would cause FailFast). Mirror aggregate progress into per-file fields, compute speeds/time left safely, and log swallowed exceptions. - Update StartPackageVerification comment to note verification is included in preload, and relax an OperationCanceledException catch condition to always handle cancellations. - Add additional debug/error logging throughout to aid troubleshooting.
1 parent 01f4364 commit 41500b5

File tree

1 file changed

+154
-49
lines changed

1 file changed

+154
-49
lines changed

CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs

Lines changed: 154 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,73 @@ public async ValueTask<int> GetInstallationPath(bool isHasOnlyMigrateOption = fa
196196
return folder;
197197
}
198198

199-
public Task StartPackageDownload(bool skipDialog = false)
199+
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
200+
public async Task StartPackageDownload(bool skipDialog = false)
200201
{
201-
// NOP
202-
return Task.CompletedTask;
202+
ResetStatusAndProgressProperty();
203+
204+
try
205+
{
206+
IsRunning = true;
207+
208+
Status.IsProgressAllIndetermined = true;
209+
Status.IsProgressPerFileIndetermined = true;
210+
Status.IsRunning = true;
211+
Status.IsIncludePerFileIndicator = false;
212+
UpdateStatus();
213+
214+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Step 1/5: InitPluginComAsync...", LogType.Debug, true);
215+
await _gameInstaller.InitPluginComAsync(_plugin, Token!.Token);
216+
217+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Step 2/5: GetGameSizeAsync...", LogType.Debug, true);
218+
Guid cancelGuid = _plugin.RegisterCancelToken(Token.Token);
219+
220+
_gameInstaller.GetGameSizeAsync(GameInstallerKind.Preload, in cancelGuid, out nint asyncSize);
221+
long sizeToDownload = await asyncSize.AsTask<long>();
222+
Logger.LogWriteLine($"[PluginGameInstallWrapper::StartPackageDownload] Size to download: {sizeToDownload}", LogType.Debug, true);
223+
224+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Step 3/5: GetGameDownloadedSizeAsync...", LogType.Debug, true);
225+
_gameInstaller.GetGameDownloadedSizeAsync(GameInstallerKind.Preload, in cancelGuid, out nint asyncDownloaded);
226+
long sizeAlreadyDownloaded = await asyncDownloaded.AsTask<long>();
227+
Logger.LogWriteLine($"[PluginGameInstallWrapper::StartPackageDownload] Already downloaded: {sizeAlreadyDownloaded}", LogType.Debug, true);
228+
229+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Step 4/5: EnsureDiskSpaceAvailability...", LogType.Debug, true);
230+
await EnsureDiskSpaceAvailability(GameManager.GameDirPath, sizeToDownload, sizeAlreadyDownloaded);
231+
232+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Step 5/5: StartPreloadAsync...", LogType.Debug, true);
233+
_gameInstaller.StartPreloadAsync(
234+
_updateProgressDelegate,
235+
_updateProgressStatusDelegate,
236+
_plugin.RegisterCancelToken(Token.Token),
237+
out nint asyncPreload);
238+
239+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Awaiting preload task...", LogType.Debug, true);
240+
await asyncPreload.AsTask().ConfigureAwait(false);
241+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Preload task completed.", LogType.Debug, true);
242+
}
243+
catch (OperationCanceledException)
244+
{
245+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Cancelled by user.", LogType.Warning, true);
246+
Status.IsCanceled = true;
247+
throw;
248+
}
249+
catch (Exception ex)
250+
{
251+
Logger.LogWriteLine($"[PluginGameInstallWrapper::StartPackageDownload] Preload failed:\r\n{ex}", LogType.Error, true);
252+
SentryHelper.ExceptionHandler(ex, SentryHelper.ExceptionType.UnhandledOther);
253+
}
254+
finally
255+
{
256+
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Entering finally block.", LogType.Debug, true);
257+
Status.IsCompleted = true;
258+
IsRunning = false;
259+
UpdateStatus();
260+
}
203261
}
204262

205263
public ValueTask<int> StartPackageVerification(List<GameInstallPackage>? gamePackage = null)
206264
{
207-
// NOP
265+
// NOP — preload download includes verification internally
208266
return new ValueTask<int>(1);
209267
}
210268

@@ -262,7 +320,7 @@ public async Task StartPackageInstallation()
262320

263321
await routineTask.ConfigureAwait(false);
264322
}
265-
catch (OperationCanceledException) when (Token!.IsCancellationRequested)
323+
catch (OperationCanceledException)
266324
{
267325
Status.IsCanceled = true;
268326
throw;
@@ -277,64 +335,93 @@ public async Task StartPackageInstallation()
277335

278336
private void UpdateProgressCallback(in InstallProgress delegateProgress)
279337
{
280-
using (_updateStatusLock.EnterScope())
338+
// IMPORTANT: This callback is invoked via a function pointer from the NativeAOT plugin.
339+
// Any unhandled exception here crosses the reverse P/Invoke boundary and causes a
340+
// FailFast (STATUS_FAIL_FAST_EXCEPTION / exit code -1073741189). Must never throw.
341+
try
281342
{
282-
_updateProgressProperty.StateCount = delegateProgress.StateCount;
283-
_updateProgressProperty.StateCountTotal = delegateProgress.TotalStateToComplete;
343+
using (_updateStatusLock.EnterScope())
344+
{
345+
_updateProgressProperty.StateCount = delegateProgress.StateCount;
346+
_updateProgressProperty.StateCountTotal = delegateProgress.TotalStateToComplete;
284347

285-
_updateProgressProperty.AssetCount = delegateProgress.DownloadedCount;
286-
_updateProgressProperty.AssetCountTotal = delegateProgress.TotalCountToDownload;
348+
_updateProgressProperty.AssetCount = delegateProgress.DownloadedCount;
349+
_updateProgressProperty.AssetCountTotal = delegateProgress.TotalCountToDownload;
287350

288-
long downloadedBytes = delegateProgress.DownloadedBytes;
289-
long downloadedBytesTotal = delegateProgress.TotalBytesToDownload;
351+
long downloadedBytes = delegateProgress.DownloadedBytes;
352+
long downloadedBytesTotal = delegateProgress.TotalBytesToDownload;
290353

291-
long readDownload = delegateProgress.DownloadedBytes - _updateProgressProperty.LastDownloaded;
292-
double currentSpeed = CalculateSpeed(readDownload);
354+
long readDownload = delegateProgress.DownloadedBytes - _updateProgressProperty.LastDownloaded;
355+
double currentSpeed = CalculateSpeed(readDownload);
293356

294-
Progress.ProgressAllSizeCurrent = downloadedBytes;
295-
Progress.ProgressAllSizeTotal = downloadedBytesTotal;
296-
Progress.ProgressAllSpeed = currentSpeed;
357+
Progress.ProgressAllSizeCurrent = downloadedBytes;
358+
Progress.ProgressAllSizeTotal = downloadedBytesTotal;
359+
Progress.ProgressAllSpeed = currentSpeed;
297360

298-
Progress.ProgressAllTimeLeft = ConverterTool
299-
.ToTimeSpanRemain(downloadedBytesTotal, downloadedBytes, currentSpeed);
361+
// Mirror into per-file fields — plugin reports aggregate progress only,
362+
// but the preload UI has a left-side panel that reads PerFile values.
363+
Progress.ProgressPerFileSizeCurrent = downloadedBytes;
364+
Progress.ProgressPerFileSizeTotal = downloadedBytesTotal;
365+
Progress.ProgressPerFileSpeed = currentSpeed;
300366

301-
Progress.ProgressAllPercentage = ConverterTool.ToPercentage(downloadedBytesTotal, downloadedBytes);
367+
Progress.ProgressAllTimeLeft = downloadedBytesTotal > 0 && currentSpeed > 0
368+
? ConverterTool.ToTimeSpanRemain(downloadedBytesTotal, downloadedBytes, currentSpeed)
369+
: TimeSpan.Zero;
302370

303-
_updateProgressProperty.LastDownloaded = downloadedBytes;
371+
Progress.ProgressAllPercentage = downloadedBytesTotal > 0
372+
? ConverterTool.ToPercentage(downloadedBytesTotal, downloadedBytes)
373+
: 0;
304374

305-
if (CheckIfNeedRefreshStopwatch())
306-
{
307-
return;
308-
}
375+
Progress.ProgressPerFilePercentage = Progress.ProgressAllPercentage;
309376

310-
if (Status.IsProgressAllIndetermined)
311-
{
312-
Status.IsProgressAllIndetermined = false;
313-
Status.IsProgressPerFileIndetermined = false;
314-
UpdateStatus();
315-
}
377+
_updateProgressProperty.LastDownloaded = downloadedBytes;
378+
379+
if (!CheckIfNeedRefreshStopwatch())
380+
{
381+
return;
382+
}
383+
384+
if (Status.IsProgressAllIndetermined)
385+
{
386+
Status.IsProgressAllIndetermined = false;
387+
Status.IsProgressPerFileIndetermined = false;
388+
UpdateStatus();
389+
}
316390

317-
UpdateProgress();
391+
UpdateProgress();
392+
}
393+
}
394+
catch (Exception ex)
395+
{
396+
Logger.LogWriteLine($"[PluginGameInstallWrapper::UpdateProgressCallback] Exception (swallowed to prevent FailFast):\r\n{ex}", LogType.Error, true);
318397
}
319398
}
320399

321400
private void UpdateStatusCallback(InstallProgressState delegateState)
322401
{
323-
using (_updateStatusLock.EnterScope())
402+
// IMPORTANT: Same reverse P/Invoke boundary guard as UpdateProgressCallback above.
403+
try
324404
{
325-
string stateString = delegateState switch
405+
using (_updateStatusLock.EnterScope())
326406
{
327-
InstallProgressState.Removing => string.Format("Deleting" + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal),
328-
InstallProgressState.Idle => Locale.Lang._Misc.Idle,
329-
InstallProgressState.Install => string.Format(Locale.Lang._Misc.Extracting + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal),
330-
InstallProgressState.Verify or InstallProgressState.Preparing => string.Format(Locale.Lang._Misc.Verifying + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal),
331-
_ => string.Format((!_updateProgressProperty.IsUpdateMode ? Locale.Lang._Misc.Downloading : Locale.Lang._Misc.Updating) + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal)
332-
};
407+
string stateString = delegateState switch
408+
{
409+
InstallProgressState.Removing => string.Format("Deleting" + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal),
410+
InstallProgressState.Idle => Locale.Lang._Misc.Idle,
411+
InstallProgressState.Install => string.Format(Locale.Lang._Misc.Extracting + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal),
412+
InstallProgressState.Verify or InstallProgressState.Preparing => string.Format(Locale.Lang._Misc.Verifying + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal),
413+
_ => string.Format((!_updateProgressProperty.IsUpdateMode ? Locale.Lang._Misc.Downloading : Locale.Lang._Misc.Updating) + ": " + Locale.Lang._Misc.PerFromTo, _updateProgressProperty.StateCount, _updateProgressProperty.StateCountTotal)
414+
};
415+
416+
Status.ActivityStatus = stateString;
417+
Status.ActivityAll = string.Format(Locale.Lang._Misc.PerFromTo, _updateProgressProperty.AssetCount, _updateProgressProperty.AssetCountTotal);
333418

334-
Status.ActivityStatus = stateString;
335-
Status.ActivityAll = string.Format(Locale.Lang._Misc.PerFromTo, _updateProgressProperty.AssetCount, _updateProgressProperty.AssetCountTotal);
336-
337-
UpdateStatus();
419+
UpdateStatus();
420+
}
421+
}
422+
catch (Exception ex)
423+
{
424+
Logger.LogWriteLine($"[PluginGameInstallWrapper::UpdateStatusCallback] Exception (swallowed to prevent FailFast):\r\n{ex}", LogType.Error, true);
338425
}
339426
}
340427

@@ -412,12 +499,30 @@ public async ValueTask<bool> UninstallGame()
412499

413500
public void Flush() => FlushingTrigger?.Invoke(this, EventArgs.Empty);
414501

415-
// TODO:
416-
// Implement this after WuWa Plugin implementation is completed
417-
public ValueTask<bool> IsPreloadCompleted(CancellationToken token = default)
502+
public async ValueTask<bool> IsPreloadCompleted(CancellationToken token = default)
418503
{
419-
// NOP
420-
return new ValueTask<bool>(true);
504+
try
505+
{
506+
await _gameInstaller.InitPluginComAsync(_plugin, token);
507+
508+
Guid cancelGuid = _plugin.RegisterCancelToken(token);
509+
510+
_gameInstaller.GetGameSizeAsync(GameInstallerKind.Preload, in cancelGuid, out nint asyncTotal);
511+
long totalSize = await asyncTotal.AsTask<long>();
512+
513+
if (totalSize <= 0)
514+
return false;
515+
516+
_gameInstaller.GetGameDownloadedSizeAsync(GameInstallerKind.Preload, in cancelGuid, out nint asyncDownloaded);
517+
long downloadedSize = await asyncDownloaded.AsTask<long>();
518+
519+
return downloadedSize >= totalSize;
520+
}
521+
catch (Exception ex)
522+
{
523+
Logger.LogWriteLine($"[PluginGameInstallWrapper::IsPreloadCompleted] Error checking preload status:\r\n{ex}", LogType.Error, true);
524+
return false;
525+
}
421526
}
422527

423528
// TODO:

0 commit comments

Comments
 (0)