diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs index 08556982f..4420f336a 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/GameInstallPackage.cs @@ -15,21 +15,22 @@ namespace CollapseLauncher.InstallManager internal class GameInstallPackage : IAssetIndexSummary { #region Properties - public string URL { get; set; } - public string DecompressedURL { get; set; } - public string Name { get; set; } - public string PathOutput { get; set; } - public GameInstallPackageType PackageType { get; set; } - public long Size { get; set; } - public long SizeRequired { get; set; } - public long SizeDownloaded { get; set; } - public GameVersion Version { get; set; } - public byte[] Hash { get; set; } - public string HashString { get => HexTool.BytesToHexUnsafe(Hash); } - public string LanguageID { get; set; } - public List Segments { get; set; } - public string RunCommand { get; set; } - public string PluginId { get; set; } + public string URL { get; set; } + public string DecompressedURL { get; set; } + public string Name { get; set; } + public string PathOutput { get; set; } + public GameInstallPackageType PackageType { get; set; } + public long Size { get; set; } + public long SizeRequired { get; set; } + public long SizeDownloaded { get; set; } + public GameVersion Version { get; set; } + public byte[] Hash { get; set; } + public string HashString { get => HexTool.BytesToHexUnsafe(Hash); } + public string LanguageID { get; set; } + public List Segments { get; set; } + public string RunCommand { get; set; } + public string PluginId { get; set; } + public bool IsUseLegacyDownloader { get; set; } #endregion public GameInstallPackage(RegionResourcePlugin packageProperty, string pathOutput) diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs index 98863b15d..45e3e7c9e 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs @@ -14,6 +14,7 @@ // ReSharper disable ConvertToPrimaryConstructor using CollapseLauncher.CustomControls; +using CollapseLauncher.Dialogs; using CollapseLauncher.Extension; using CollapseLauncher.FileDialogCOM; using CollapseLauncher.Helper; @@ -23,6 +24,7 @@ using Hi3Helper; using Hi3Helper.Data; using Hi3Helper.EncTool.Parser.AssetIndex; +using Hi3Helper.Http; using Hi3Helper.Http.Legacy; using Hi3Helper.Shared.ClassStruct; using Hi3Helper.Shared.Region; @@ -59,7 +61,6 @@ using SophonLogger = Hi3Helper.Sophon.Helper.Logger; using SophonManifest = Hi3Helper.Sophon.SophonManifest; -using CollapseLauncher.Dialogs; // ReSharper disable ForCanBeConvertedToForeach // ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault @@ -522,6 +523,15 @@ protected virtual async ValueTask StartDeltaPatchPreReqDownload(List x!.Segments != null ? x.Segments.Count : 1) > 1; _status.IsProgressPerFileIndetermined = true; @@ -533,7 +543,7 @@ protected virtual async ValueTask StartDeltaPatchPreReqDownload(List x!.Size); - _progressAllSizeCurrent = GetExistingDownloadPackageSize(gamePackage); + _progressAllSizeCurrent = await GetExistingDownloadPackageSize(downloadClient, gamePackage, _token!.Token); // Sanitize Check: Check for the free space of the drive and show the dialog if necessary await CheckDriveFreeSpace(_parentUI, gamePackage, _progressAllSizeCurrent); @@ -547,7 +557,7 @@ protected virtual async ValueTask StartDeltaPatchPreReqDownload(List x!.Segments != null ? x.Segments.Count : 1) > 1; @@ -669,7 +688,7 @@ public virtual async Task StartPackageDownload(bool skipDialog) // Get the remote total size and current total size _progressAllSizeTotal = _assetIndex!.Sum(x => x!.Size); - _progressAllSizeCurrent = GetExistingDownloadPackageSize(_assetIndex); + _progressAllSizeCurrent = await GetExistingDownloadPackageSize(downloadClient, _assetIndex, _token!.Token); // Sanitize Check: Check for the free space of the drive and show the dialog if necessary await CheckDriveFreeSpace(_parentUI, _assetIndex, _progressAllSizeCurrent); @@ -681,7 +700,7 @@ public virtual async Task StartPackageDownload(bool skipDialog) } // Start downloading process - await InvokePackageDownloadRoutine(_assetIndex, _token!.Token); + await InvokePackageDownloadRoutine(downloadClient, _assetIndex, _token!.Token); } // Bool: 0 -> Indicates that one of the package is failing and need to redownload @@ -3470,7 +3489,8 @@ private void TryRemoveRedundantHDiffList() #region Private Methods - StartPackageDownload - private async ValueTask InvokePackageDownloadRoutine(List packageList, + private async ValueTask InvokePackageDownloadRoutine(DownloadClient downloadClient, + List packageList, CancellationToken token) { // Get the package/segment count @@ -3481,16 +3501,15 @@ private async ValueTask InvokePackageDownloadRoutine(List pa _progressAllCountTotal = packageCount; RestartStopwatch(); - // Initialize new proxy-aware HttpClient - using HttpClient httpClientNew = new HttpClientBuilder() - .UseLauncherConfig(_downloadThreadCount + 16) - .SetAllowedDecompression(DecompressionMethods.None) - .Create(); - - using Http _httpClient = new Http(true, customHttpClient: httpClientNew); + // Initialize a legacy Http as well + using Http _httpClient = new Http(true, customHttpClient: downloadClient.GetHttpClient()); // Subscribe the download progress to the event adapter _httpClient.DownloadProgress += HttpClientDownloadProgressAdapter; + + // Create a speed limiter instance for the new DownloadClient and register it + DownloadSpeedLimiter speedLimiter = DownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached); + LauncherConfig.DownloadSpeedLimitChanged += speedLimiter.GetListener(); try { // Iterate the package list @@ -3502,7 +3521,7 @@ private async ValueTask InvokePackageDownloadRoutine(List pa // Iterate the segment list for (int i = 0; i < package.Segments.Count; i++) { - await RunPackageDownloadRoutine(_httpClient, package.Segments[i], token, packageCount); + await RunPackageDownloadRoutine(_httpClient, downloadClient, speedLimiter, package.Segments[i], token, packageCount); } // Skip action below and continue to the next segment @@ -3510,19 +3529,25 @@ private async ValueTask InvokePackageDownloadRoutine(List pa } // Else, run the routine as normal - await RunPackageDownloadRoutine(_httpClient, package, token, packageCount); + await RunPackageDownloadRoutine(_httpClient, downloadClient, speedLimiter, package, token, packageCount); } } finally { // Unsubscribe the download progress from the event adapter _httpClient.DownloadProgress -= HttpClientDownloadProgressAdapter; + + // Unregister the speed limiter + LauncherConfig.DownloadSpeedLimitChanged -= speedLimiter.GetListener(); } } - private async ValueTask RunPackageDownloadRoutine(Http httpClient, - GameInstallPackage package, CancellationToken token, - int packageCount) + private async ValueTask RunPackageDownloadRoutine(Http httpClient, + DownloadClient downloadClient, + DownloadSpeedLimiter downloadSpeedLimiter, + GameInstallPackage package, + CancellationToken token, + int packageCount) { // Set the activity status _status.IsIncludePerFileIndicator = packageCount > 1; @@ -3539,16 +3564,37 @@ private async ValueTask RunPackageDownloadRoutine(Http httpClient, if (!isExistingPackageFileExist || existingPackageFileSize != package.Size) { - // If the package size is more than or equal to 10 MB, then allow to use multi-session. - // Otherwise, forcefully use single-session. - bool isCanMultiSession = package.Size >= 10 << 20; - if (isCanMultiSession) + // Get the file path + string filePath = EnsureCreationOfDirectory(package.PathOutput); + + bool isCanMultiSession = false; + // If a legacy downloader is used, then use the legacy Http downloader + if (package.IsUseLegacyDownloader) { - await httpClient.Download(package.URL, EnsureCreationOfDirectory(package.PathOutput), _downloadThreadCount, false, token); + // If the package size is more than or equal to 10 MB, then allow to use multi-session. + // Otherwise, forcefully use single-session. + isCanMultiSession = package.Size >= 10 << 20; + if (isCanMultiSession) + { + await httpClient.Download(package.URL, filePath, _downloadThreadCount, false, token); + } + else + { + await httpClient.Download(package.URL, filePath, false, null, null, token); + } } + // Otherwise, use the new downloder else { - await httpClient.Download(package.URL, EnsureCreationOfDirectory(package.PathOutput), false, null, null, token); + // Run the new downloader + await RunDownloadTask( + package.Size, + filePath, + package.URL, + downloadClient, + HttpClientDownloadProgressAdapter, + token, + false); } // Update status to merging @@ -3559,8 +3605,8 @@ private async ValueTask RunPackageDownloadRoutine(Http httpClient, _stopwatch.Stop(); // Check if the merge chunk is enabled and the download could perform multisession, - // then do merge. - if (_canMergeDownloadChunks && isCanMultiSession) + // then do merge (also if legacy downloader is used). + if (_canMergeDownloadChunks && isCanMultiSession && package.IsUseLegacyDownloader) { await httpClient.Merge(token); } @@ -3628,11 +3674,21 @@ private void DeleteDownloadedFile(string FileOutput, byte Thread) fileInfo.Delete(); } + // Get the info of the new downloader metadata + FileInfo fileInfoMetadata = new FileInfo(FileOutput + ".collapseMeta"); + + // If metadata file existed, then delete + if (fileInfoMetadata.Exists) + { + fileInfoMetadata.IsReadOnly = false; + fileInfoMetadata.Delete(); + } + // Delete the file of the chunk file too Http.DeleteMultisessionFiles(FileOutput, Thread); } - private long GetExistingDownloadPackageSize(List packageList) + private async ValueTask GetExistingDownloadPackageSize(DownloadClient downloadClient, List packageList, CancellationToken token) { // Initialize total existing size and download thread count long totalSize = 0; @@ -3649,19 +3705,37 @@ private long GetExistingDownloadPackageSize(List packageList Http .CalculateExistingMultisessionFilesWithExpctdSize(packageList[i].Segments[j].PathOutput, _downloadThreadCount, packageList[i].Segments[j].Size); - totalSize += segmentDownloaded; - totalSegmentDownloaded += segmentDownloaded; - packageList[i].Segments[j].SizeDownloaded = segmentDownloaded; + + long newSegmentedDownloaded = await downloadClient.GetDownloadedFileSize( + packageList[i].Segments[j].URL, + packageList[i].Segments[j].PathOutput, + packageList[i].Segments[j].Size, + token + ); + bool isUseLegacySegmentedSize = segmentDownloaded > newSegmentedDownloaded || newSegmentedDownloaded > packageList[i].Segments[j].Size; + totalSize += isUseLegacySegmentedSize ? segmentDownloaded : newSegmentedDownloaded; + totalSegmentDownloaded += isUseLegacySegmentedSize ? segmentDownloaded : newSegmentedDownloaded; + packageList[i].Segments[j].SizeDownloaded = isUseLegacySegmentedSize ? segmentDownloaded : newSegmentedDownloaded; + packageList[i].Segments[j].IsUseLegacyDownloader = isUseLegacySegmentedSize; } packageList[i].SizeDownloaded = totalSegmentDownloaded; continue; } - packageList[i].SizeDownloaded = + long legacyDownloadedSize = Http.CalculateExistingMultisessionFilesWithExpctdSize(packageList[i].PathOutput, _downloadThreadCount, packageList[i].Size); - totalSize += packageList[i].SizeDownloaded; + long newDownloaderSize = await downloadClient.GetDownloadedFileSize( + packageList[i].URL, + packageList[i].PathOutput, + packageList[i].Size, + token + ); + bool isUseLegacySize = legacyDownloadedSize > newDownloaderSize || newDownloaderSize > packageList[i].Size; + packageList[i].IsUseLegacyDownloader = isUseLegacySize; + packageList[i].SizeDownloaded = isUseLegacySize ? legacyDownloadedSize : newDownloaderSize; + totalSize += packageList[i].SizeDownloaded; } // return totalSize @@ -3936,6 +4010,63 @@ private long GetLastSize(long input) return a; } + private async void HttpClientDownloadProgressAdapter(int read, DownloadProgress downloadProgress) + { + // Set the progress bar not indetermined + _status.IsProgressPerFileIndetermined = false; + _status.IsProgressAllIndetermined = false; + + // Increment the total current size if status is not merging + Interlocked.Add(ref _progressAllSizeCurrent, read); + // Increment the total last read + Interlocked.Add(ref _progressAllIOReadCurrent, read); + + if (_refreshStopwatch!.ElapsedMilliseconds > _refreshInterval) + { + // Assign local sizes to progress + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + + // Calculate the speed + double speedPerFile = _progressAllIOReadCurrent / _downloadSpeedRefreshStopwatch.Elapsed.TotalSeconds; + double speedAllFile = (_progressAllSizeCurrent / _stopwatch.Elapsed.TotalSeconds).ClampLimitedSpeedNumber(); + _progress.ProgressAllSpeed = speedPerFile.ClampLimitedSpeedNumber(); + + // Calculate percentage + _progress.ProgressPerFilePercentage = + Math.Round(_progressPerFileSizeCurrent / (double)_progressPerFileSizeTotal * 100, 2); + _progress.ProgressAllPercentage = + Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); + + // Calculate the timelapse + _progress.ProgressAllTimeLeft = + ((_progressAllSizeTotal - _progressAllSizeCurrent) / speedAllFile.Unzeroed()) + .ToTimeSpanNormalized(); + + // Update the status of per file size and current progress from Http client + _progressPerFileSizeCurrent = downloadProgress.BytesDownloaded; + _progressPerFileSizeTotal = downloadProgress.BytesTotal; + _progress.ProgressPerFilePercentage = ConverterTool.GetPercentageNumber(downloadProgress.BytesDownloaded, downloadProgress.BytesTotal); + + lock (_downloadSpeedRefreshStopwatch) + { + if (_downloadSpeedRefreshInterval < _downloadSpeedRefreshStopwatch!.ElapsedMilliseconds) + { + _progressAllIOReadCurrent = 0; + _downloadSpeedRefreshStopwatch.Restart(); + } + } + + // Update the status + UpdateAll(); + + _refreshStopwatch.Restart(); + await Task.Delay(_refreshInterval); + } + } + private async void HttpClientDownloadProgressAdapter(object sender, DownloadEvent e) { // Set the progress bar not indetermined diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index 281af07f5..7b3f917a2 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -64,7 +64,7 @@ private void Init() protected long _progressAllSizeCurrent; protected long _progressAllSizeFound; protected long _progressAllSizeTotal; - protected double _progressAllIOReadCurrent; + protected long _progressAllIOReadCurrent; protected long _progressPerFileSizeCurrent; protected long _progressPerFileSizeTotal; protected double _progressPerFileIOReadCurrent; @@ -955,20 +955,40 @@ protected bool SummarizeStatusAndProgress(List assetIndex, string msgIfFound #nullable enable protected virtual async Task RunDownloadTask(long assetSize, string assetPath, string assetURL, - DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token, bool isOverwrite = true, - EventHandler? downloadSpeedLimitChanged = null) + DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token, bool isOverwrite = true) { - // Always do multi-session download with the new DownloadClient regardless of any sizes (if applicable) - await downloadClient.DownloadAsync( - assetURL, - EnsureCreationOfDirectory(assetPath), - isOverwrite, - sessionChunkSize: LauncherConfig.DownloadChunkSize, - progressDelegateAsync: downloadProgress, - cancelToken: token, - downloadSpeedLimitEvent: downloadSpeedLimitChanged, - initialDownloadSpeed: LauncherConfig.DownloadSpeedLimitCached - ); + // For any instances that uses Burst Download and if the speed limiter is null when + // _isBurstDownloadEnabled set to false, then create the speed limiter instance + bool isUseSelfSpeedLimiter = !_isBurstDownloadEnabled; + DownloadSpeedLimiter? downloadSpeedLimiter = null; + if (isUseSelfSpeedLimiter) + { + // Create the speed limiter instance and register the listener + downloadSpeedLimiter = DownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached); + LauncherConfig.DownloadSpeedLimitChanged += downloadSpeedLimiter.GetListener(); + } + + try + { + // Always do multi-session download with the new DownloadClient regardless of any sizes (if applicable) + await downloadClient.DownloadAsync( + assetURL, + EnsureCreationOfDirectory(assetPath), + isOverwrite, + sessionChunkSize: LauncherConfig.DownloadChunkSize, + progressDelegateAsync: downloadProgress, + cancelToken: token, + downloadSpeedLimiter: downloadSpeedLimiter + ); + } + finally + { + // If the self speed listener is used, then unregister the listener + if (isUseSelfSpeedLimiter && downloadSpeedLimiter != null) + { + LauncherConfig.DownloadSpeedLimitChanged -= downloadSpeedLimiter?.GetListener(); + } + } } #nullable restore diff --git a/Hi3Helper.Http b/Hi3Helper.Http index 2dc55ec52..2af2e27a3 160000 --- a/Hi3Helper.Http +++ b/Hi3Helper.Http @@ -1 +1 @@ -Subproject commit 2dc55ec52841201cbbf9d8047d6f412a89c259b9 +Subproject commit 2af2e27a3ed5f40a10c38246dc333cd241aec263