diff --git a/CollapseLauncher/App.xaml b/CollapseLauncher/App.xaml index 29062f0d2..9e5269d99 100644 --- a/CollapseLauncher/App.xaml +++ b/CollapseLauncher/App.xaml @@ -385,13 +385,13 @@ + Color="#60000000" /> + Color="#00E00000" /> + Color="#FFE00000" /> @@ -744,9 +744,9 @@ + Color="#00FF6666" /> + Color="#FFFF0000" /> diff --git a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs index 2ba8dca7a..2307e172b 100644 --- a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs +++ b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs @@ -1,6 +1,7 @@ using CommunityToolkit.WinUI; using Hi3Helper; using Hi3Helper.CommunityToolkit.WinUI.Controls; +using Hi3Helper.SentryHelper; using Microsoft.UI; using Microsoft.UI.Input; using Microsoft.UI.Text; @@ -17,7 +18,6 @@ using System.Runtime.CompilerServices; using Windows.UI; using Windows.UI.Text; -using Hi3Helper.SentryHelper; namespace CollapseLauncher.Extension { diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/ScreenManager.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/ScreenManager.cs index c21b587a1..dc1f77f86 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/ScreenManager.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Genshin/RegistryClass/ScreenManager.cs @@ -1,9 +1,9 @@ using CollapseLauncher.GameSettings.Base; -using CollapseLauncher.Helper; using CollapseLauncher.Interfaces; using Hi3Helper; using Hi3Helper.EncTool; using Microsoft.Win32; +using Hi3Helper.Win32.Screen; using System; using System.Drawing; using static CollapseLauncher.GameSettings.Base.SettingsBase; @@ -17,7 +17,7 @@ internal class ScreenManager : BaseScreenSettingData, IGameSettingsValue { #region Fields - private const string _ValueName = "GENERAL_DATA_V2_ScreenSettingData_h1916288658"; + private const string _ValueName = "GENERAL_DATA_V2_ScreenSettingData_h1916288658"; private const string _ValueNameScreenManagerFullscreen = "Screenmanager Is Fullscreen mode_h3981298716"; - private const string _ValueNameScreenManagerWidth = "Screenmanager Resolution Width_h182942802"; - private const string _ValueNameScreenManagerHeight = "Screenmanager Resolution Height_h2627697771"; - private static Size currentRes = WindowUtility.CurrentScreenProp.CurrentResolution; + private const string _ValueNameScreenManagerWidth = "Screenmanager Resolution Width_h182942802"; + private const string _ValueNameScreenManagerHeight = "Screenmanager Resolution Height_h2627697771"; + private static Size currentRes = ScreenProp.CurrentResolution; #endregion #region Properties diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs index 4cb156bfc..0c32d4644 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/StarRail/RegistryClass/PCResolution.cs @@ -1,6 +1,5 @@ using CollapseLauncher.GameSettings.Base; using CollapseLauncher.GameSettings.StarRail.Context; -using CollapseLauncher.Helper; using CollapseLauncher.Interfaces; using Hi3Helper; using Hi3Helper.EncTool; @@ -10,6 +9,7 @@ using System.Text; using System.Text.Json.Serialization; using Hi3Helper.SentryHelper; +using Hi3Helper.Win32.Screen; using static CollapseLauncher.GameSettings.Base.SettingsBase; using static Hi3Helper.Logger; @@ -18,11 +18,11 @@ namespace CollapseLauncher.GameSettings.StarRail internal class PCResolution : BaseScreenSettingData, IGameSettingsValue { #region Fields - private const string _ValueName = "GraphicsSettings_PCResolution_h431323223"; - private const string _ValueNameScreenManagerWidth = "Screenmanager Resolution Width_h182942802"; - private const string _ValueNameScreenManagerHeight = "Screenmanager Resolution Height_h2627697771"; + private const string _ValueName = "GraphicsSettings_PCResolution_h431323223"; + private const string _ValueNameScreenManagerWidth = "Screenmanager Resolution Width_h182942802"; + private const string _ValueNameScreenManagerHeight = "Screenmanager Resolution Height_h2627697771"; private const string _ValueNameScreenManagerFullscreen = "Screenmanager Fullscreen mode_h3630240806"; - private static Size currentRes = WindowUtility.CurrentScreenProp.CurrentResolution; + private static Size currentRes = ScreenProp.CurrentResolution; #endregion #region Properties diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/RegistryClass/ScreenManager.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/RegistryClass/ScreenManager.cs index c49a02815..a2f795a86 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/RegistryClass/ScreenManager.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/RegistryClass/ScreenManager.cs @@ -4,6 +4,7 @@ using Hi3Helper; using Hi3Helper.EncTool; using Microsoft.Win32; +using Hi3Helper.Win32.Screen; using System; using System.Drawing; using Hi3Helper.SentryHelper; @@ -20,7 +21,7 @@ internal class ScreenManager : BaseScreenSettingData, IGameSettingsValue + /// Manages the thread pool settings to throttle the number of threads. + /// + internal partial class ThreadPoolThrottle : IDisposable + { + private readonly int PreviousThreadCount; + private readonly int PreviousCompletionPortThreadCount; + internal readonly int MultipliedThreadCount; + + /// + /// Initializes a new instance of the class. + /// + /// The previous maximum number of worker threads. + /// The previous maximum number of asynchronous I/O threads. + /// The multiplied thread count. + private ThreadPoolThrottle(int previousThreadCount, int previousCompletionPortThreadCount, int multipliedThreadCount) + { + PreviousThreadCount = previousThreadCount; + PreviousCompletionPortThreadCount = previousCompletionPortThreadCount; + MultipliedThreadCount = multipliedThreadCount; + } + + /// + /// Starts the thread pool throttle by setting the maximum number of threads. + /// + /// The factor to multiply the processor count by to determine the maximum number of threads. + /// A instance that can be used to restore the previous thread pool settings. + public static ThreadPoolThrottle Start(int multiply = 4) + { + var threadCount = Environment.ProcessorCount * multiply; + ThreadPool.GetMinThreads(out var workerThreads, out var completionPortThreads); + ThreadPool.SetMinThreads(Math.Max(workerThreads, threadCount), + Math.Max(completionPortThreads, threadCount)); + return new ThreadPoolThrottle(workerThreads, completionPortThreads, threadCount); + } + + /// + /// Restores the previous thread pool settings. + /// + public void Dispose() + { + ThreadPool.SetMaxThreads(PreviousThreadCount, PreviousCompletionPortThreadCount); + } + } +} diff --git a/CollapseLauncher/Classes/Helper/WindowUtility.cs b/CollapseLauncher/Classes/Helper/WindowUtility.cs index e0b68d2d6..7952e3029 100644 --- a/CollapseLauncher/Classes/Helper/WindowUtility.cs +++ b/CollapseLauncher/Classes/Helper/WindowUtility.cs @@ -54,7 +54,6 @@ internal static class WindowUtility internal static AppWindow? CurrentAppWindow; internal static WindowId? CurrentWindowId; internal static OverlappedPresenter? CurrentOverlappedPresenter; - internal static ScreenProp? CurrentScreenProp; internal static DisplayArea? CurrentWindowDisplayArea { @@ -665,7 +664,7 @@ private static IntPtr DesktopSiteBridgeWndProc(IntPtr hwnd, uint msg, UIntPtr wP internal static void SetWindowSize(int width, int height) { - if (CurrentScreenProp == null || CurrentWindowPtr == nint.Zero) + if (CurrentWindowPtr == nint.Zero) return; // Get the scale factor and calculate the size and offset @@ -673,7 +672,7 @@ internal static void SetWindowSize(int width, int height) int lastWindowWidth = (int)(width * scaleFactor); int lastWindowHeight = (int)(height * scaleFactor); - Size desktopSize = CurrentScreenProp.GetScreenSize(); + Size desktopSize = ScreenProp.CurrentResolution; int xOff = desktopSize.Width / 2 - lastWindowWidth / 2; int yOff = desktopSize.Height / 2 - lastWindowHeight / 2; diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs index 70e8a451a..77c76a0e6 100644 --- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs +++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs @@ -123,6 +123,13 @@ protected struct UninstallGameProperty // TODO: Override if the game was supposed to have voice packs (For example: Genshin) protected virtual int _gameVoiceLanguageID => int.MinValue; + protected virtual string[] _gameVoiceLanguageLocaleIdOrdered => [ + "zh-cn", + "en-us", + "ja-jp", + "ko-kr" + ]; + protected virtual string _gameDataPath => Path.Combine(_gamePath, $"{Path.GetFileNameWithoutExtension(_gameVersionManager.GamePreset.GameExecutableName)}_Data"); @@ -805,205 +812,218 @@ public virtual async Task StartPackageInstallSophon(GameInstallStateEnum gameSta .UseLauncherConfig(maxHttpHandler) .Create(); - try + using (ThreadPoolThrottle.Start()) { - // Reset status and progress properties - ResetStatusAndProgress(); + try + { + // Reset status and progress properties + ResetStatusAndProgress(); - // Clear the VO language list - _sophonVOLanguageList?.Clear(); + // Clear the VO language list + _sophonVOLanguageList?.Clear(); - // Subscribe the logger event - SophonLogger.LogHandler += UpdateSophonLogHandler; + // Subscribe the logger event + SophonLogger.LogHandler += UpdateSophonLogHandler; - // Get the requested URL and version based on current state. - if (_gameVersionManager.GamePreset - .LauncherResourceChunksURL != null) - { - #nullable enable - // Reassociate the URL if branch url exist - string? branchUrl = _gameVersionManager.GamePreset - .LauncherResourceChunksURL - .BranchUrl; - if (!string.IsNullOrEmpty(branchUrl) - && !string.IsNullOrEmpty(_gameVersionManager.GamePreset.LauncherBizName)) + // Get the requested URL and version based on current state. + if (_gameVersionManager.GamePreset + .LauncherResourceChunksURL != null) { - await _gameVersionManager.GamePreset - .LauncherResourceChunksURL - .EnsureReassociated( - httpClient, - branchUrl, - _gameVersionManager.GamePreset.LauncherBizName, - _token.Token); - } - #nullable restore +#nullable enable + // Reassociate the URL if branch url exist + string? branchUrl = _gameVersionManager.GamePreset + .LauncherResourceChunksURL + .BranchUrl; + if (!string.IsNullOrEmpty(branchUrl) + && !string.IsNullOrEmpty(_gameVersionManager.GamePreset.LauncherBizName)) + { + await _gameVersionManager.GamePreset + .LauncherResourceChunksURL + .EnsureReassociated( + httpClient, + branchUrl, + _gameVersionManager.GamePreset.LauncherBizName, + _token.Token); + } +#nullable restore - #if SIMULATEAPPLYPRELOAD - string requestedUrl = gameState switch - { - GameInstallStateEnum.InstalledHavePreload => _gameVersionManager.GamePreset - .LauncherResourceChunksURL.PreloadUrl, - _ => _gameVersionManager.GamePreset.LauncherResourceChunksURL.MainUrl - }; - GameVersion? requestedVersion = gameState switch - { - GameInstallStateEnum.InstalledHavePreload => _gameVersionManager! - .GetGameVersionAPIPreload(), - _ => _gameVersionManager!.GetGameVersionAPIPreload() - } ?? _gameVersionManager!.GetGameVersionAPI(); - #else - string requestedUrl = gameState switch - { - GameInstallStateEnum.InstalledHavePreload => _gameVersionManager - .GamePreset - .LauncherResourceChunksURL.PreloadUrl, - _ => _gameVersionManager.GamePreset.LauncherResourceChunksURL.MainUrl - }; - GameVersion? requestedVersion = gameState switch - { - GameInstallStateEnum.InstalledHavePreload => - _gameVersionManager! - .GetGameVersionAPIPreload(), - _ => _gameVersionManager!.GetGameVersionAPI() - } ?? _gameVersionManager!.GetGameVersionAPI(); - - // Add the tag query to the Url - requestedUrl += $"&tag={requestedVersion.ToString()}"; - #endif +#if SIMULATEAPPLYPRELOAD + string requestedUrl = gameState switch + { + GameInstallStateEnum.InstalledHavePreload => _gameVersionManager.GamePreset + .LauncherResourceChunksURL.PreloadUrl, + _ => _gameVersionManager.GamePreset.LauncherResourceChunksURL.MainUrl + }; + GameVersion? requestedVersion = gameState switch + { + GameInstallStateEnum.InstalledHavePreload => _gameVersionManager! + .GetGameVersionAPIPreload(), + _ => _gameVersionManager!.GetGameVersionAPIPreload() + } ?? _gameVersionManager!.GetGameVersionAPI(); +#else + string requestedUrl = gameState switch + { + GameInstallStateEnum.InstalledHavePreload => _gameVersionManager + .GamePreset + .LauncherResourceChunksURL.PreloadUrl, + _ => _gameVersionManager.GamePreset.LauncherResourceChunksURL.MainUrl + }; + GameVersion? requestedVersion = gameState switch + { + GameInstallStateEnum.InstalledHavePreload => + _gameVersionManager! + .GetGameVersionAPIPreload(), + _ => _gameVersionManager!.GetGameVersionAPI() + } ?? _gameVersionManager!.GetGameVersionAPI(); + + // Add the tag query to the Url + requestedUrl += $"&tag={requestedVersion.ToString()}"; +#endif - // Set the progress bar to indetermined - _status.IsIncludePerFileIndicator = false; - _status.IsProgressPerFileIndetermined = false; - _status.IsProgressAllIndetermined = true; - UpdateStatus(); + // Set the progress bar to indetermined + _status.IsIncludePerFileIndicator = false; + _status.IsProgressPerFileIndetermined = false; + _status.IsProgressAllIndetermined = true; + UpdateStatus(); - // Initialize the info pair list - var sophonInfoPairList = new List(); + // Initialize the info pair list + var sophonInfoPairList = new List(); - // Get the info pair based on info provided above (for main game file) - var sophonMainInfoPair = await - SophonManifest.CreateSophonChunkManifestInfoPair(httpClient, requestedUrl, "game", - _token.Token) - .ConfigureAwait(false); - sophonInfoPairList.Add(sophonMainInfoPair); + // Get the info pair based on info provided above (for main game file) + var sophonMainInfoPair = await + SophonManifest.CreateSophonChunkManifestInfoPair(httpClient, requestedUrl, "game", _token.Token); - List voLanguageList = - GetSophonLanguageDisplayDictFromVoicePackList(sophonMainInfoPair.OtherSophonData); + // Ensure that the manifest is ordered based on _gameVoiceLanguageLocaleIdOrdered + RearrangeSophonDataLocaleOrder(sophonMainInfoPair.OtherSophonData); - Dispatch( async void () => - { - try - { - (List addedVO, int setAsDefaultVO) = - await Dialog_ChooseAudioLanguageChoice(_parentUI, voLanguageList); - if (addedVO == null || setAsDefaultVO < 0) - { - throw new TaskCanceledException(); - } - - for (int i = 0; i < addedVO.Count; i++) - { - int voLangIndex = addedVO[i]; - string voLangLocaleCode = GetLanguageLocaleCodeByID(voLangIndex); - _sophonVOLanguageList?.Add(voLangLocaleCode); - - // Get the info pair based on info provided above (for the selected VO audio file) - SophonChunkManifestInfoPair sophonSelectedVoLang = - sophonMainInfoPair.GetOtherManifestInfoPair(voLangLocaleCode); - sophonInfoPairList.Add(sophonSelectedVoLang); - } - - // Set the voice language ID to value given - _gameVersionManager.GamePreset.SetVoiceLanguageID(setAsDefaultVO); - - // Get the remote total size and current total size - _progressAllCountTotal = sophonInfoPairList.Sum(x => x.ChunksInfo.FilesCount); - _progressAllSizeTotal = sophonInfoPairList.Sum(x => x.ChunksInfo.TotalSize); - _progressAllSizeCurrent = 0; - - // Set the display to Install Mode - _isSophonInUpdateMode = false; - - // Set the progress bar to indetermined - _status.IsIncludePerFileIndicator = false; - _status.IsProgressPerFileIndetermined = false; - _status.IsProgressAllIndetermined = false; - UpdateStatus(); - } - catch (Exception e) - { - ErrorSender.SendException(e); - } - }); - - // Get the parallel options - var parallelOptions = new ParallelOptions - { - MaxDegreeOfParallelism = maxThread, - CancellationToken = _token.Token - }; - var parallelChunksOptions = new ParallelOptions - { - MaxDegreeOfParallelism = maxChunksThread, - CancellationToken = _token.Token - }; - - // Declare the download delegate - async ValueTask DelegateAssetDownload(SophonAsset asset, CancellationToken _) - { - // ReSharper disable once AccessToDisposedClosure - await RunSophonAssetDownloadThread(httpClient, asset, parallelChunksOptions); - } + // Add the manifest to the pair list + sophonInfoPairList.Add(sophonMainInfoPair); - // Declare the rename temp file delegate - async ValueTask DelegateAssetRenameTempFile(SophonAsset asset, CancellationToken token) - { - await Task.Run(() => - { - // If the asset is a dictionary, then return - if (asset.IsDirectory) - { - return; - } - - // Get the file path and start the write process - var assetName = asset.AssetName; - var filePath = new FileInfo( - EnsureCreationOfDirectory(Path.Combine(_gamePath, assetName)) + - "_tempSophon").EnsureNoReadOnly(); - var origFilePath = new FileInfo(Path.Combine(_gamePath, assetName)).EnsureNoReadOnly(); - - if (filePath.Exists) - { - filePath.MoveTo(origFilePath.FullName, true); - filePath.Refresh(); - origFilePath.Refresh(); - } - }, token); - } + List voLanguageList = + GetSophonLanguageDisplayDictFromVoicePackList(sophonMainInfoPair.OtherSophonData); - // Enumerate the asset in parallel and start the download process - await RunTaskAction(httpClient, sophonInfoPairList, parallelOptions, DelegateAssetDownload); + // Get Audio Choices first + (List addedVO, int setAsDefaultVO) = + await Dialog_ChooseAudioLanguageChoice(voLanguageList, GetSophonLocaleCodeIndex(sophonMainInfoPair.OtherSophonData, "ja-jp")); - // Rename temporary files - await RunTaskAction(httpClient, sophonInfoPairList, parallelOptions, DelegateAssetRenameTempFile); + try + { + if (addedVO == null || setAsDefaultVO < 0) + { + throw new TaskCanceledException(); + } - // Remove sophon verified files - CleanupTempSophonVerifiedFiles(); - } + for (int i = 0; i < addedVO.Count; i++) + { + int voLangIndex = addedVO[i]; + string voLangLocaleCode = GetLanguageLocaleCodeByID(voLangIndex); + _sophonVOLanguageList?.Add(voLangLocaleCode); + + // Get the info pair based on info provided above (for the selected VO audio file) + SophonChunkManifestInfoPair sophonSelectedVoLang = + sophonMainInfoPair.GetOtherManifestInfoPair(voLangLocaleCode); + sophonInfoPairList.Add(sophonSelectedVoLang); + } - _isSophonDownloadCompleted = true; - } - finally - { - // Unsubscribe the logger event - SophonLogger.LogHandler -= UpdateSophonLogHandler; - httpClient.Dispose(); + // Set the voice language ID to value given + _gameVersionManager.GamePreset.SetVoiceLanguageID(setAsDefaultVO); + + // Get the remote total size and current total size + _progressAllCountTotal = sophonInfoPairList.Sum(x => x.ChunksInfo.FilesCount); + _progressAllSizeTotal = sophonInfoPairList.Sum(x => x.ChunksInfo.TotalSize); + _progressAllSizeCurrent = 0; + + // Set the display to Install Mode + _isSophonInUpdateMode = false; + + // Set the progress bar to indetermined + _status.IsIncludePerFileIndicator = false; + _status.IsProgressPerFileIndetermined = false; + _status.IsProgressAllIndetermined = false; + UpdateStatus(); + } + catch (TaskCanceledException) + { + throw; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception e) + { + ErrorSender.SendException(e); + } + + // Get the parallel options + var parallelOptions = new ParallelOptions + { + MaxDegreeOfParallelism = maxThread, + CancellationToken = _token.Token + }; + var parallelChunksOptions = new ParallelOptions + { + MaxDegreeOfParallelism = maxChunksThread, + CancellationToken = _token.Token + }; + + // Declare the download delegate + async ValueTask DelegateAssetDownload(SophonAsset asset, CancellationToken _) + { + // ReSharper disable once AccessToDisposedClosure + await RunSophonAssetDownloadThread(httpClient, asset, parallelChunksOptions); + } + + // Declare the rename temp file delegate + async ValueTask DelegateAssetRenameTempFile(SophonAsset asset, CancellationToken token) + { + await Task.Run(() => + { + // If the asset is a dictionary, then return + if (asset.IsDirectory) + { + return; + } + + // Get the file path and start the write process + var assetName = asset.AssetName; + var filePath = new FileInfo( + EnsureCreationOfDirectory(Path.Combine(_gamePath, assetName)) + + "_tempSophon").EnsureNoReadOnly(); + var origFilePath = new FileInfo(Path.Combine(_gamePath, assetName)).EnsureNoReadOnly(); + + if (filePath.Exists) + { + filePath.MoveTo(origFilePath.FullName, true); + filePath.Refresh(); + origFilePath.Refresh(); + } + }, token); + } + + // Enumerate the asset in parallel and start the download process + await RunTaskAction(httpClient, sophonInfoPairList, parallelOptions, DelegateAssetDownload); + + // Rename temporary files + await RunTaskAction(httpClient, sophonInfoPairList, parallelOptions, DelegateAssetRenameTempFile); + + // Remove sophon verified files + CleanupTempSophonVerifiedFiles(); + } + + _isSophonDownloadCompleted = true; + } + finally + { + // Unsubscribe the logger event + SophonLogger.LogHandler -= UpdateSophonLogHandler; + httpClient.Dispose(); + } } return; - async Task RunTaskAction(HttpClient client, List sophonInfoPairList, + async Task RunTaskAction(HttpClient client, List sophonInfoPairListLocal, ParallelOptions parallelOptions, Func actionDelegate) { @@ -1014,7 +1034,8 @@ async Task RunTaskAction(HttpClient client, List so { LauncherConfig.DownloadSpeedLimitChanged += downloadSpeedLimiter.GetListener(); var processingInfoPair = new ConcurrentDictionary(); - foreach (SophonChunkManifestInfoPair sophonDownloadInfoPair in sophonInfoPairList) + var infoPairListCopy = sophonInfoPairListLocal.ToList(); + foreach (SophonChunkManifestInfoPair sophonDownloadInfoPair in infoPairListCopy) { if (!processingInfoPair.TryAdd(sophonDownloadInfoPair.ChunksInfo, 0)) { @@ -1272,11 +1293,13 @@ private async Task AddSophonDiffAssetsToList(HttpClient httpClie { // Get the manifest pair for both previous (from) and next (to) version SophonChunkManifestInfoPair requestPairFrom = await SophonManifest - .CreateSophonChunkManifestInfoPair(httpClient, requestedUrlFrom, matchingField, _token.Token) - .ConfigureAwait(false); + .CreateSophonChunkManifestInfoPair(httpClient, requestedUrlFrom, matchingField, _token.Token); SophonChunkManifestInfoPair requestPairTo = await SophonManifest - .CreateSophonChunkManifestInfoPair(httpClient, requestedUrlTo, matchingField, _token.Token) - .ConfigureAwait(false); + .CreateSophonChunkManifestInfoPair(httpClient, requestedUrlTo, matchingField, _token.Token); + + // Ensure that the manifest is ordered based on _gameVoiceLanguageLocaleIdOrdered + RearrangeSophonDataLocaleOrder(requestPairFrom.OtherSophonData); + RearrangeSophonDataLocaleOrder(requestPairTo.OtherSophonData); // Add asset to the list await foreach (SophonAsset sophonAsset in SophonUpdate @@ -1737,10 +1760,10 @@ private async Task ExtractUsingNativeZipWorker(IEnumerable entriesIndex, L continue; } - string outputPath = EnsureCreationOfDirectory(Path.Combine(_gamePath, zipEntry.Key)); + string outputPath = Path.Combine(_gamePath, zipEntry.Key); + FileInfo outputFile = new FileInfo(outputPath).EnsureCreationOfDirectory().EnsureNoReadOnly(); - await using FileStream outputStream = - new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Write); + await using FileStream outputStream = outputFile.Open(FileMode.Create, FileAccess.Write, FileShare.Write); await using Stream entryStream = zipEntry.OpenEntryStream(); Task runningTask = Task.Factory.StartNew( @@ -1768,9 +1791,9 @@ void StartWriteInner(byte[] bufferInner, FileStream outputStream, Stream entrySt // Increment total size _progressAllSizeCurrent += read; _progressPerFileSizeCurrent += read; - + // Calculate the speed - _progress.ProgressAllSpeed = CalculateSpeed(read); + lock (_progress) _progress.ProgressAllSpeed = CalculateSpeed(read); if (!CheckIfNeedRefreshStopwatch()) { @@ -1778,20 +1801,23 @@ void StartWriteInner(byte[] bufferInner, FileStream outputStream, Stream entrySt } // Assign local sizes to progress - _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - - // Calculate percentage - _progress.ProgressAllPercentage = - Math.Round((double)_progressAllSizeCurrent / _progressAllSizeTotal * 100, 2); - _progress.ProgressPerFilePercentage = - Math.Round((double)_progressPerFileSizeCurrent / _progressPerFileSizeTotal * 100, 2); - // Calculate the timelapse - _progress.ProgressAllTimeLeft = - ((_progressAllSizeTotal - _progressAllSizeCurrent) / _progress.ProgressAllSpeed.Unzeroed()) - .ToTimeSpanNormalized(); + lock (_progress) + { + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + + // Calculate percentage + _progress.ProgressAllPercentage = + Math.Round((double)_progressAllSizeCurrent / _progressAllSizeTotal * 100, 2); + _progress.ProgressPerFilePercentage = + Math.Round((double)_progressPerFileSizeCurrent / _progressPerFileSizeTotal * 100, 2); + // Calculate the timelapse + _progress.ProgressAllTimeLeft = + ((_progressAllSizeTotal - _progressAllSizeCurrent) / _progress.ProgressAllSpeed.Unzeroed()) + .ToTimeSpanNormalized(); + } UpdateAll(); } @@ -2094,7 +2120,9 @@ public async ValueTask UninstallGame() string appDataPath = _gameVersionManager.GameDirAppDataPath; try { - Directory.Delete(appDataPath, true); + if (Directory.Exists(appDataPath)) + Directory.Delete(appDataPath, true); + LogWriteLine($"Deleted {appDataPath}", LogType.Default, true); } catch (Exception ex) @@ -2293,11 +2321,14 @@ private async ValueTask FileHdiffPatcherInner(string patchPath, string sourceBas public virtual async ValueTask ApplyHdiffListPatch() { List hdiffEntry = TryGetHDiffList(); - - _progress.ProgressAllSizeTotal = hdiffEntry.Sum(x => x.fileSize); - _progress.ProgressAllSizeCurrent = 0; _status.IsIncludePerFileIndicator = false; + lock (_progress) + { + _progress.ProgressAllSizeTotal = hdiffEntry.Sum(x => x.fileSize); + _progress.ProgressAllSizeCurrent = 0; + } + _progressAllCountTotal = 1; _progressAllCountFound = hdiffEntry.Count; @@ -2346,14 +2377,15 @@ public virtual async ValueTask ApplyHdiffListPatch() lock (_progress) { _progress.ProgressAllSizeCurrent += entry.fileSize; + _progress.ProgressAllPercentage = + Math.Round(_progress.ProgressAllSizeCurrent / _progress.ProgressAllSizeTotal * 100, 2); + _progress.ProgressAllSpeed = CalculateSpeed(entry.fileSize); + + _progress.ProgressAllTimeLeft = + ((_progress.ProgressAllSizeTotal - _progress.ProgressAllSizeCurrent) / + _progress.ProgressAllSpeed.Unzeroed()).ToTimeSpanNormalized(); } - _progress.ProgressAllPercentage = - Math.Round(_progress.ProgressAllSizeCurrent / _progress.ProgressAllSizeTotal * 100, 2); - _progress.ProgressAllSpeed = CalculateSpeed(entry.fileSize); - _progress.ProgressAllTimeLeft = - ((_progress.ProgressAllSizeTotal - _progress.ProgressAllSizeCurrent) / - _progress.ProgressAllSpeed.Unzeroed()).ToTimeSpanNormalized(); UpdateProgress(); } finally @@ -2408,13 +2440,16 @@ private void EventListener_PatchEvent(object sender, PatchEvent e) } if (CheckIfNeedRefreshStopwatch()) { - _progress.ProgressAllPercentage = - Math.Round(_progress.ProgressAllSizeCurrent / _progress.ProgressAllSizeTotal * 100, 2); - _progress.ProgressAllSpeed = CalculateSpeed(e.Read); + lock (_progress) + { + _progress.ProgressAllPercentage = + Math.Round(_progress.ProgressAllSizeCurrent / _progress.ProgressAllSizeTotal * 100, 2); + _progress.ProgressAllSpeed = CalculateSpeed(e.Read); - _progress.ProgressAllTimeLeft = - ((_progress.ProgressAllSizeTotal - _progress.ProgressAllSizeCurrent) / - _progress.ProgressAllSpeed.Unzeroed()).ToTimeSpanNormalized(); + _progress.ProgressAllTimeLeft = + ((_progress.ProgressAllSizeTotal - _progress.ProgressAllSizeCurrent) / + _progress.ProgressAllSpeed.Unzeroed()).ToTimeSpanNormalized(); + } UpdateProgress(); } } @@ -2581,22 +2616,15 @@ protected virtual string GetLanguageDisplayByLocaleCode(string localeCode, bool }; } - protected virtual List GetLanguageDisplayListFromVoicePackList(List voicePacks) + protected virtual int GetSophonLocaleCodeIndex(SophonData sophonData, string lookupName) { - List value = []; - foreach (RegionResourceVersion Entry in voicePacks) - { - // Check the lang ID and add the translation of the language to the list - string languageDisplay = GetLanguageDisplayByLocaleCode(Entry.language, false); - if (string.IsNullOrEmpty(languageDisplay)) - { - continue; - } - - value.Add(languageDisplay); - } + List localeList = sophonData.ManifestIdentityList + .Where(x => IsValidLocaleCode(x.MatchingField)) + .Select(x => x.MatchingField.ToLower()) + .ToList(); - return value; + int index = localeList.IndexOf(lookupName); + return Math.Max(0, index); } protected virtual Dictionary GetLanguageDisplayDictFromVoicePackList( @@ -2643,6 +2671,54 @@ protected virtual List GetSophonLanguageDisplayDictFromVoicePackList(Sop return value; } + protected virtual void RearrangeLegacyPackageLocaleOrder(RegionResourceVersion regionResource) + { + // Rearrange the region resource list order based on matching field for the locale + RearrangeDataListLocaleOrder(regionResource.voice_packs, x => x.language); + } + + protected virtual void RearrangeSophonDataLocaleOrder(SophonData sophonData) + { + // Rearrange the sophon data list order based on matching field for the locale + RearrangeDataListLocaleOrder(sophonData.ManifestIdentityList, x => x.MatchingField); + } + + protected virtual void RearrangeDataListLocaleOrder(List assetDataList, Func matchingFieldPredicate) + { + // Get ordered locale string + string[] localeStringOrder = _gameVoiceLanguageLocaleIdOrdered; + + // Separate non-locale and locale manifest list + List manifestListMain = assetDataList + .Where(x => !IsValidLocaleCode(matchingFieldPredicate(x))) + .ToList(); + List manifestListLocale = assetDataList. + Where(x => IsValidLocaleCode(matchingFieldPredicate(x))) + .ToList(); + + // SLOW: Order the locale manifest list by the localeStringOrder + for (int i = 0; i < localeStringOrder.Length; i++) + { + var localeFound = manifestListLocale.FirstOrDefault(x => matchingFieldPredicate(x).Equals(localeStringOrder[i], StringComparison.OrdinalIgnoreCase)); + if (localeFound != null) + { + // Move from main to locale + manifestListMain.Add(localeFound); + manifestListLocale.Remove(localeFound); + } + } + + // Add the rest of the unknown locale if exist + if (manifestListLocale.Count != 0) + { + manifestListMain.AddRange(manifestListLocale); + } + + // Rearrange by cleaning the list and re-add the sorted list + assetDataList.Clear(); + assetDataList.AddRange(manifestListMain); + } + protected virtual bool TryGetVoiceOverResourceByLocaleCode(List verResList, string localeCode, out RegionResourceVersion outRes) { @@ -3267,6 +3343,7 @@ private async Task GetLatestPackageList(List packageList, Ga continue; } + RearrangeLegacyPackageLocaleOrder(asset); await TryAddResourceVersionList(asset, packageList); } } @@ -3985,8 +4062,11 @@ public void UpdateCompletenessStatus(CompletenessStatus status) InnerLauncherConfig.AppDiscordPresence?.SetActivity(ActivityType.Idle); #endif // HACK: Fix the progress not achieving 100% while completed - _progress.ProgressAllPercentage = 100f; - _progress.ProgressPerFilePercentage = 100f; + lock (_progress) + { + _progress.ProgressAllPercentage = 100f; + _progress.ProgressPerFilePercentage = 100f; + } break; case CompletenessStatus.Cancelled: IsRunning = false; @@ -4061,13 +4141,16 @@ protected void UpdateProgressBase() protected void DeltaPatchCheckProgress(object sender, PatchEvent e) { - _progress.ProgressAllPercentage = e.ProgressPercentage; + lock (_progress) + { + _progress.ProgressAllPercentage = e.ProgressPercentage; - _progress.ProgressAllTimeLeft = e.TimeLeft; - _progress.ProgressAllSpeed = e.Speed; + _progress.ProgressAllTimeLeft = e.TimeLeft; + _progress.ProgressAllSpeed = e.Speed; - _progress.ProgressAllSizeTotal = e.TotalSizeToBePatched; - _progress.ProgressAllSizeCurrent = e.CurrentSizePatched; + _progress.ProgressAllSizeTotal = e.TotalSizeToBePatched; + _progress.ProgressAllSizeCurrent = e.CurrentSizePatched; + } if (CheckIfNeedRefreshStopwatch()) { @@ -4105,14 +4188,17 @@ protected void DeltaPatchCheckLogEvent(object sender, LoggerEvent e) protected void DeltaPatchCheckProgress(object sender, TotalPerFileProgress e) { - _progress.ProgressAllPercentage = - e.ProgressAllPercentage == 0 ? e.ProgressPerFilePercentage : e.ProgressAllPercentage; + lock (_progress) + { + _progress.ProgressAllPercentage = + e.ProgressAllPercentage == 0 ? e.ProgressPerFilePercentage : e.ProgressAllPercentage; - _progress.ProgressAllTimeLeft = e.ProgressAllTimeLeft; - _progress.ProgressAllSpeed = e.ProgressAllSpeed; + _progress.ProgressAllTimeLeft = e.ProgressAllTimeLeft; + _progress.ProgressAllSpeed = e.ProgressAllSpeed; - _progress.ProgressAllSizeTotal = e.ProgressAllSizeTotal; - _progress.ProgressAllSizeCurrent = e.ProgressAllSizeCurrent; + _progress.ProgressAllSizeTotal = e.ProgressAllSizeTotal; + _progress.ProgressAllSizeCurrent = e.ProgressAllSizeCurrent; + } if (CheckIfNeedRefreshStopwatch()) { @@ -4134,24 +4220,27 @@ private void ZipProgressAdapter(object sender, ExtractProgressProp e) _progressPerFileSizeCurrent = (long)e.TotalRead; _progressPerFileSizeTotal = (long)e.TotalSize; - // Assign local sizes to progress - _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + lock (_progress) + { + // Assign local sizes to progress + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - // Calculate the speed - _progress.ProgressAllSpeed = CalculateSpeed(lastSize); + // Calculate the speed + _progress.ProgressAllSpeed = CalculateSpeed(lastSize); - // Calculate percentage - _progress.ProgressAllPercentage = - Math.Round((double)_progressAllSizeCurrent / _progressAllSizeTotal * 100, 2); - _progress.ProgressPerFilePercentage = - Math.Round((double)_progressPerFileSizeCurrent / _progressPerFileSizeTotal * 100, 2); - // Calculate the timelapse - _progress.ProgressAllTimeLeft = - ((_progressAllSizeTotal - _progressAllSizeCurrent) / _progress.ProgressAllSpeed.Unzeroed()) - .ToTimeSpanNormalized(); + // Calculate percentage + _progress.ProgressAllPercentage = + Math.Round((double)_progressAllSizeCurrent / _progressAllSizeTotal * 100, 2); + _progress.ProgressPerFilePercentage = + Math.Round((double)_progressPerFileSizeCurrent / _progressPerFileSizeTotal * 100, 2); + // Calculate the timelapse + _progress.ProgressAllTimeLeft = + ((_progressAllSizeTotal - _progressAllSizeCurrent) / _progress.ProgressAllSpeed.Unzeroed()) + .ToTimeSpanNormalized(); + } UpdateAll(); } @@ -4183,31 +4272,35 @@ private void HttpClientDownloadProgressAdapter(int read, DownloadProgress downlo if (CheckIfNeedRefreshStopwatch()) { - // Assign local sizes to progress - _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - - // Assign speed with clamped value - double speedClamped = speedAll.ClampLimitedSpeedNumber(); - _progress.ProgressAllSpeed = speedClamped; + lock (_progress) + { + // Assign local sizes to progress + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - // Calculate percentage - _progress.ProgressPerFilePercentage = - Math.Round(_progressPerFileSizeCurrent / (double)_progressPerFileSizeTotal * 100, 2); - _progress.ProgressAllPercentage = - Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); + // Assign speed with clamped value + double speedClamped = speedAll.ClampLimitedSpeedNumber(); + _progress.ProgressAllSpeed = speedClamped; - // Calculate the timelapse - double progressTimeAvg = (_progressAllSizeTotal - _progressAllSizeCurrent) / speedClamped; - _progress.ProgressAllTimeLeft = progressTimeAvg.ToTimeSpanNormalized(); + // Calculate percentage + _progress.ProgressPerFilePercentage = + Math.Round(_progressPerFileSizeCurrent / (double)_progressPerFileSizeTotal * 100, 2); + _progress.ProgressAllPercentage = + Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); - // 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); + // Calculate the timelapse + double progressTimeAvg = (_progressAllSizeTotal - _progressAllSizeCurrent) / speedClamped; + _progress.ProgressAllTimeLeft = progressTimeAvg.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); + } // Update the status UpdateAll(); } @@ -4232,25 +4325,28 @@ private void HttpClientDownloadProgressAdapter(object sender, DownloadEvent e) { if (e.State != DownloadState.Merging) { - // Assign local sizes to progress - _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - - // Calculate the speed - _progress.ProgressAllSpeed = speedAll; - - // 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) / _progress.ProgressAllSpeed.Unzeroed()) - .ToTimeSpanNormalized(); + lock (_progress) + { + // Assign local sizes to progress + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + + // Calculate the speed + _progress.ProgressAllSpeed = speedAll; + + // 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) / _progress.ProgressAllSpeed.Unzeroed()) + .ToTimeSpanNormalized(); + } } else { @@ -4260,22 +4356,25 @@ private void HttpClientDownloadProgressAdapter(object sender, DownloadEvent e) // If status is merging, then use progress for speed and timelapse from Http client // and set the rest from the base class - _progress.ProgressAllTimeLeft = e.TimeLeft; + lock (_progress) + { + _progress.ProgressAllTimeLeft = e.TimeLeft; - _progress.ProgressAllSpeed = speedAll; + _progress.ProgressAllSpeed = speedAll; - _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; - _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; - _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; - _progress.ProgressAllSizeTotal = _progressAllSizeTotal; - _progress.ProgressAllPercentage = - Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); + _progress.ProgressPerFileSizeCurrent = _progressPerFileSizeCurrent; + _progress.ProgressPerFileSizeTotal = _progressPerFileSizeTotal; + _progress.ProgressAllSizeCurrent = _progressAllSizeCurrent; + _progress.ProgressAllSizeTotal = _progressAllSizeTotal; + _progress.ProgressAllPercentage = + Math.Round(_progressAllSizeCurrent / (double)_progressAllSizeTotal * 100, 2); + } } // Update the status of per file size and current progress from Http client _progressPerFileSizeCurrent = e.SizeDownloaded; _progressPerFileSizeTotal = e.SizeToBeDownloaded; - _progress.ProgressPerFilePercentage = e.ProgressPercentage; + lock (_progress) _progress.ProgressPerFilePercentage = e.ProgressPercentage; // Update the status UpdateAll(); diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index e7aee12a8..1d340802d 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -2,6 +2,7 @@ using CollapseLauncher.Dialogs; using CollapseLauncher.Extension; using CollapseLauncher.Helper; +using CommunityToolkit.WinUI; using Hi3Helper; using Hi3Helper.Data; using Hi3Helper.Http; @@ -21,12 +22,14 @@ using System.IO.Hashing; using System.Linq; using System.Net.Http; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Hi3Helper.SentryHelper; using static Hi3Helper.Locale; using static Hi3Helper.Logger; +using CollapseUIExtension = CollapseLauncher.Extension.UIElementExtensions; #nullable enable namespace CollapseLauncher.Interfaces @@ -675,16 +678,18 @@ protected void ResetStatusAndProgressProperty() _status.IsCanceled = false; // Reset all total activity progress - _progress.ProgressPerFilePercentage = 0; - _progress.ProgressAllPercentage = 0; - _progress.ProgressPerFileSpeed = 0; - _progress.ProgressAllSpeed = 0; - - _progress.ProgressAllEntryCountCurrent = 0; - _progress.ProgressAllEntryCountTotal = 0; - _progress.ProgressPerFileEntryCountCurrent = 0; - _progress.ProgressPerFileEntryCountTotal = 0; - + lock (_progress) + { + _progress.ProgressPerFilePercentage = 0; + _progress.ProgressAllPercentage = 0; + _progress.ProgressPerFileSpeed = 0; + _progress.ProgressAllSpeed = 0; + + _progress.ProgressAllEntryCountCurrent = 0; + _progress.ProgressAllEntryCountTotal = 0; + _progress.ProgressPerFileEntryCountCurrent = 0; + _progress.ProgressPerFileEntryCountTotal = 0; + } // Reset all inner counter _progressAllCountCurrent = 0; _progressAllCountTotal = 0; @@ -748,7 +753,9 @@ protected async ValueTask FetchBilibiliSDK(CancellationToken token) // Get the URL and get the remote stream of the zip file // Also buffer the stream to memory string? url = _gameVersionManager.GameAPIProp.data.sdk.path; - using HttpResponseMessage httpResponse = await FallbackCDNUtil.GetURLHttpResponse(url, token); + if (url == null) throw new NullReferenceException(); + + HttpResponseMessage httpResponse = await FallbackCDNUtil.GetURLHttpResponse(url, token); await using BridgedNetworkStream httpStream = await FallbackCDNUtil.GetHttpStreamFromResponse(httpResponse, token); await using MemoryStream bufferedStream = await BufferSourceStreamToMemoryStream(httpStream, token); using ZipArchive zip = new ZipArchive(bufferedStream, ZipArchiveMode.Read, true); @@ -1168,7 +1175,7 @@ protected async Task SpawnRepairDialog(List assetIndex, Action? actionIfInte { ArgumentNullException.ThrowIfNull(assetIndex); long totalSize = assetIndex.Sum(x => x.GetAssetSize()); - StackPanel content = UIElementExtensions.CreateStackPanel(); + StackPanel content = CollapseUIExtension.CreateStackPanel(); content.AddElementToStackPanel(new TextBlock() { @@ -1177,7 +1184,7 @@ protected async Task SpawnRepairDialog(List assetIndex, Action? actionIfInte TextWrapping = TextWrapping.Wrap }); Button showBrokenFilesButton = content.AddElementToStackPanel( - UIElementExtensions.CreateButtonWithIcon