Skip to content

Commit 6a71b12

Browse files
committed
Add per-file progress callback support
Register a native per-file progress callback for plugins and use it to populate per-file progress fields. - Introduces an unmanaged Cdecl delegate and GCHandle to hold a reverse P/Invoke callback (OnPerFileProgressCallback) and safe registration/unregistration helpers (TryRegisterPerFileProgressCallback, UnregisterPerFileProgressCallback). - Calls registration before starting preload/download and ensures unregistration in finally blocks. - OnPerFileProgressCallback updates per-file bytes/percentage inside a try/catch (must not throw when invoked from native AOT). - Adjusts progress reporting: when the plugin provides per-file updates, use that data (and only mirror speed from aggregate); otherwise fall back to mirroring aggregate values into per-file fields. - Adds PluginInfo.EnablePerFileProgressCallback and DisablePerFileProgressCallback which call the plugin export SetPerFileProgressCallback (Cdecl) and log failures.
1 parent f5995e9 commit 6a71b12

File tree

2 files changed

+117
-7
lines changed

2 files changed

+117
-7
lines changed

CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using System.Collections.Generic;
2121
using System.IO;
2222
using System.Runtime.CompilerServices;
23+
using System.Runtime.InteropServices;
2324
using System.Threading;
2425
using System.Threading.Tasks;
2526

@@ -28,6 +29,9 @@ namespace CollapseLauncher.Plugins;
2829
#nullable enable
2930
internal partial class PluginGameInstallWrapper : ProgressBase<PkgVersionProperties>, IGameInstallManager
3031
{
32+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
33+
private unsafe delegate void PerFileProgressCallbackNative(InstallPerFileProgress* progress);
34+
3135
private struct InstallProgressProperty
3236
{
3337
public int StateCount;
@@ -61,6 +65,10 @@ public override string GamePath
6165
private readonly InstallProgressStateDelegate _updateProgressStatusDelegate;
6266
private InstallProgressProperty _updateProgressProperty;
6367

68+
private PerFileProgressCallbackNative? _perFileProgressDelegate;
69+
private GCHandle _perFileProgressGcHandle;
70+
private bool _hasPerFileProgress;
71+
6472
private PluginGameVersionWrapper GameManager =>
6573
GameVersionManager as PluginGameVersionWrapper ?? throw new InvalidCastException("GameVersionManager is not PluginGameVersionWrapper");
6674

@@ -101,10 +109,60 @@ private void ResetAndCancelTokenSource()
101109

102110
public void Dispose()
103111
{
112+
UnregisterPerFileProgressCallback();
104113
_gameInstaller.Free();
105114
GC.SuppressFinalize(this);
106115
}
107116

117+
private void TryRegisterPerFileProgressCallback()
118+
{
119+
_perFileProgressDelegate = OnPerFileProgressCallback;
120+
_perFileProgressGcHandle = GCHandle.Alloc(_perFileProgressDelegate);
121+
nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_perFileProgressDelegate);
122+
123+
_hasPerFileProgress = _pluginPresetConfig.PluginInfo.EnablePerFileProgressCallback(callbackPtr);
124+
125+
if (!_hasPerFileProgress)
126+
{
127+
_perFileProgressGcHandle.Free();
128+
_perFileProgressDelegate = null;
129+
}
130+
}
131+
132+
private void UnregisterPerFileProgressCallback()
133+
{
134+
if (!_hasPerFileProgress)
135+
return;
136+
137+
_pluginPresetConfig.PluginInfo.DisablePerFileProgressCallback();
138+
_hasPerFileProgress = false;
139+
140+
if (_perFileProgressGcHandle.IsAllocated)
141+
_perFileProgressGcHandle.Free();
142+
_perFileProgressDelegate = null;
143+
}
144+
145+
private unsafe void OnPerFileProgressCallback(InstallPerFileProgress* progress)
146+
{
147+
// IMPORTANT: Called from NativeAOT plugin across reverse P/Invoke. Must never throw.
148+
try
149+
{
150+
long downloaded = progress->PerFileDownloadedBytes;
151+
long total = progress->PerFileTotalBytes;
152+
153+
Progress.ProgressPerFileSizeCurrent = downloaded;
154+
Progress.ProgressPerFileSizeTotal = total;
155+
Progress.ProgressPerFilePercentage = total > 0
156+
? ConverterTool.ToPercentage(total, downloaded)
157+
: 0;
158+
}
159+
catch (Exception ex)
160+
{
161+
Logger.LogWriteLine($"[PluginGameInstallWrapper::OnPerFileProgressCallback] Exception (swallowed):\r\n{ex}",
162+
LogType.Error, true);
163+
}
164+
}
165+
108166
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
109167
public async ValueTask<int> GetInstallationPath(bool isHasOnlyMigrateOption = false)
110168
{
@@ -230,6 +288,8 @@ public async Task StartPackageDownload(bool skipDialog = false)
230288
await EnsureDiskSpaceAvailability(GameManager.GameDirPath, sizeToDownload, sizeAlreadyDownloaded);
231289

232290
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Step 5/5: StartPreloadAsync...", LogType.Debug, true);
291+
TryRegisterPerFileProgressCallback();
292+
233293
_gameInstaller.StartPreloadAsync(
234294
_updateProgressDelegate,
235295
_updateProgressStatusDelegate,
@@ -254,6 +314,7 @@ public async Task StartPackageDownload(bool skipDialog = false)
254314
finally
255315
{
256316
Logger.LogWriteLine("[PluginGameInstallWrapper::StartPackageDownload] Entering finally block.", LogType.Debug, true);
317+
UnregisterPerFileProgressCallback();
257318
Status.IsCompleted = true;
258319
IsRunning = false;
259320
UpdateStatus();
@@ -296,6 +357,8 @@ public async Task StartPackageInstallation()
296357

297358
await EnsureDiskSpaceAvailability(GameManager.GameDirPath, sizeToDownload, sizeAlreadyDownloaded);
298359

360+
TryRegisterPerFileProgressCallback();
361+
299362
Task routineTask;
300363
if (_updateProgressProperty.IsUpdateMode)
301364
{
@@ -327,6 +390,7 @@ public async Task StartPackageInstallation()
327390
}
328391
finally
329392
{
393+
UnregisterPerFileProgressCallback();
330394
Status.IsCompleted = true;
331395
IsRunning = false;
332396
UpdateStatus();
@@ -358,11 +422,22 @@ private void UpdateProgressCallback(in InstallProgress delegateProgress)
358422
Progress.ProgressAllSizeTotal = downloadedBytesTotal;
359423
Progress.ProgressAllSpeed = currentSpeed;
360424

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;
425+
if (_hasPerFileProgress)
426+
{
427+
// V1Ext_Update5: per-file bytes/percentage come from OnPerFileProgressCallback.
428+
// We only set the speed here (overall throughput is the meaningful metric).
429+
Progress.ProgressPerFileSpeed = currentSpeed;
430+
}
431+
else
432+
{
433+
// Fallback: mirror aggregate values into per-file fields for older plugins.
434+
Progress.ProgressPerFileSizeCurrent = downloadedBytes;
435+
Progress.ProgressPerFileSizeTotal = downloadedBytesTotal;
436+
Progress.ProgressPerFileSpeed = currentSpeed;
437+
Progress.ProgressPerFilePercentage = downloadedBytesTotal > 0
438+
? ConverterTool.ToPercentage(downloadedBytesTotal, downloadedBytes)
439+
: 0;
440+
}
366441

367442
Progress.ProgressAllTimeLeft = downloadedBytesTotal > 0 && currentSpeed > 0
368443
? ConverterTool.ToTimeSpanRemain(downloadedBytesTotal, downloadedBytes, currentSpeed)
@@ -372,8 +447,6 @@ private void UpdateProgressCallback(in InstallProgress delegateProgress)
372447
? ConverterTool.ToPercentage(downloadedBytesTotal, downloadedBytes)
373448
: 0;
374449

375-
Progress.ProgressPerFilePercentage = Progress.ProgressAllPercentage;
376-
377450
_updateProgressProperty.LastDownloaded = downloadedBytes;
378451

379452
if (!CheckIfNeedRefreshStopwatch())

CollapseLauncher/Classes/Plugins/PluginInfo.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,43 @@ internal unsafe void ToggleSpeedLimiterService(bool isEnable)
329329
}
330330
}
331331

332+
/// <summary>
333+
/// Registers a per-file progress callback with the plugin via the V1Ext_Update5 export.
334+
/// Returns <c>true</c> if the plugin supports Update5 and the callback was registered.
335+
/// </summary>
336+
internal unsafe bool EnablePerFileProgressCallback(nint callbackPtr)
337+
{
338+
if (!IsLoaded)
339+
return false;
340+
341+
if (!Handle.TryGetExportUnsafe("SetPerFileProgressCallback", out nint setCallbackP))
342+
return false;
343+
344+
HResult hr = ((delegate* unmanaged[Cdecl]<nint, HResult>)setCallbackP)(callbackPtr);
345+
if (Marshal.GetExceptionForHR(hr) is { } exception)
346+
{
347+
Logger.LogWriteLine($"[PluginInfo] Plugin: {Name} failed to register per-file progress callback: {hr} {exception}",
348+
LogType.Error, true);
349+
return false;
350+
}
351+
352+
return true;
353+
}
354+
355+
/// <summary>
356+
/// Unregisters the per-file progress callback from the plugin.
357+
/// </summary>
358+
internal unsafe void DisablePerFileProgressCallback()
359+
{
360+
if (!IsLoaded)
361+
return;
362+
363+
if (!Handle.TryGetExportUnsafe("SetPerFileProgressCallback", out nint setCallbackP))
364+
return;
365+
366+
((delegate* unmanaged[Cdecl]<nint, HResult>)setCallbackP)(nint.Zero);
367+
}
368+
332369
internal async Task Initialize(CancellationToken token = default)
333370
{
334371
if (!IsLoaded)

0 commit comments

Comments
 (0)