2020using System . Collections . Generic ;
2121using System . IO ;
2222using System . Runtime . CompilerServices ;
23+ using System . Runtime . InteropServices ;
2324using System . Threading ;
2425using System . Threading . Tasks ;
2526
@@ -28,6 +29,9 @@ namespace CollapseLauncher.Plugins;
2829#nullable enable
2930internal 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 ( ) )
0 commit comments