diff --git a/.github/workflows/qodana-scan-pr.yml b/.github/workflows/qodana-scan-pr.yml
index c114b97c7..3a0d457e8 100644
--- a/.github/workflows/qodana-scan-pr.yml
+++ b/.github/workflows/qodana-scan-pr.yml
@@ -3,6 +3,8 @@ on:
pull_request:
branches:
- main
+ - preview
+ - stable
jobs:
qodana:
diff --git a/CollapseLauncher/App.xaml b/CollapseLauncher/App.xaml
index 9e5269d99..ec4f953ec 100644
--- a/CollapseLauncher/App.xaml
+++ b/CollapseLauncher/App.xaml
@@ -5,7 +5,7 @@
xmlns:conv="using:CollapseLauncher.Pages"
xmlns:interactions="using:Microsoft.Xaml.Interactions.Core"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
- xmlns:localUI="using:Microsoft.UI.Xaml.Controls"
+ xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:primitives="using:Microsoft.UI.Xaml.Controls.Primitives"
xmlns:ui="using:Windows.UI">
@@ -1048,7 +1048,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
@@ -1083,7 +1083,7 @@
-
+
@@ -1105,7 +1105,7 @@
-
+
@@ -1128,7 +1128,7 @@
-
+
@@ -1152,7 +1152,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
@@ -1188,7 +1188,7 @@
-
+
@@ -1210,7 +1210,7 @@
-
+
@@ -1232,7 +1232,7 @@
-
+
@@ -1265,7 +1265,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderThickness="{TemplateBinding BorderThickness}"
@@ -1306,7 +1306,7 @@
-
+
@@ -1328,7 +1328,7 @@
-
+
@@ -1351,7 +1351,7 @@
-
+
@@ -1384,7 +1384,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderThickness="{TemplateBinding BorderThickness}"
@@ -1425,7 +1425,7 @@
-
+
@@ -1447,7 +1447,7 @@
-
+
@@ -1470,7 +1470,7 @@
-
+
@@ -1503,7 +1503,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderThickness="{TemplateBinding BorderThickness}"
@@ -1544,7 +1544,7 @@
-
+
@@ -1566,7 +1566,7 @@
-
+
@@ -1589,7 +1589,7 @@
-
+
@@ -1622,7 +1622,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
@@ -1657,7 +1657,7 @@
-
+
@@ -1679,7 +1679,7 @@
-
+
@@ -1707,7 +1707,7 @@
-
+
@@ -1741,7 +1741,7 @@
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
- localUI:AnimatedIcon.State="Normal"
+ controls:AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
@@ -1782,7 +1782,7 @@
-
+
@@ -5017,6 +5017,129 @@
+
+
+
+
+
+
+
+
+ 0,-6,0,0
diff --git a/CollapseLauncher/Assets/Images/GameBackground/genshin.webp b/CollapseLauncher/Assets/Images/GameBackground/genshin.webp
new file mode 100644
index 000000000..2b3825757
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/genshin.webp differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/honkai.webp b/CollapseLauncher/Assets/Images/GameBackground/honkai.webp
new file mode 100644
index 000000000..8d0ed46ea
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/honkai.webp differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/starrail.webp b/CollapseLauncher/Assets/Images/GameBackground/starrail.webp
new file mode 100644
index 000000000..149b046de
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/starrail.webp differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/zzz.webp b/CollapseLauncher/Assets/Images/GameBackground/zzz.webp
new file mode 100644
index 000000000..d1f3f1a19
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/zzz.webp differ
diff --git a/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs b/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs
index cc5982e27..e14ef5a73 100644
--- a/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs
+++ b/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs
@@ -1,4 +1,5 @@
-using CollapseLauncher.Interfaces;
+using CollapseLauncher.Helper;
+using CollapseLauncher.Interfaces;
using Hi3Helper;
using System;
using System.Collections.Generic;
@@ -57,19 +58,31 @@ private async Task> Check(List assetIndex, Cancella
private void CheckUnusedAssets(List assetIndex, List returnAsset)
{
+ // Directory info and if the directory doesn't exist, return
+ DirectoryInfo directoryInfo = new DirectoryInfo(_gamePath);
+ if (!directoryInfo.Exists)
+ {
+ return;
+ }
+
// Iterate the file contained in the _gamePath
- foreach (string filePath in Directory.EnumerateFiles(_gamePath!, "*", SearchOption.AllDirectories))
+ foreach (FileInfo fileInfo in directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories)
+ .EnumerateNoReadOnly())
{
- if (!filePath.Contains("output_log") && !filePath.Contains("Crashes")
- && !filePath.Contains("Verify.txt") && !filePath.Contains("APM")
- && !filePath.Contains("FBData") && !filePath.Contains("asb.dat")
- && !assetIndex!.Exists(x => x!.ConcatPath == filePath))
+ string filePath = fileInfo.FullName;
+
+ if (!filePath.Contains("output_log", StringComparison.OrdinalIgnoreCase)
+ && !filePath.Contains("Crashes", StringComparison.OrdinalIgnoreCase)
+ && !filePath.Contains("Verify.txt", StringComparison.OrdinalIgnoreCase)
+ && !filePath.Contains("APM", StringComparison.OrdinalIgnoreCase)
+ && !filePath.Contains("FBData", StringComparison.OrdinalIgnoreCase)
+ && !filePath.Contains("asb.dat", StringComparison.OrdinalIgnoreCase)
+ && !assetIndex.Exists(x => x.ConcatPath == fileInfo.FullName))
{
// Increment the total found count
_progressAllCountFound++;
// Add asset to the returnAsset
- FileInfo fileInfo = new FileInfo(filePath);
CacheAsset asset = new CacheAsset()
{
BasePath = Path.GetDirectoryName(filePath),
@@ -106,10 +119,10 @@ private async ValueTask CheckAsset(CacheAsset asset, List returnAsse
_status.ActivityAll = string.Format(Lang._CachesPage.CachesTotalStatusChecking!, _progressAllCountCurrent, _progressAllCountTotal);
// Assign the file info.
- FileInfo fileInfo = new FileInfo(asset.ConcatPath!);
+ FileInfo fileInfo = new FileInfo(asset.ConcatPath).EnsureNoReadOnly(out bool isExist);
// Check if the file exist. If not, then add it to asset index.
- if (!fileInfo.Exists)
+ if (!isExist)
{
AddGenericCheckAsset(asset, CacheAssetStatus.New, returnAsset, null, asset.CRCArray);
return;
diff --git a/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs b/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs
index 7f6830dcd..27c9681e2 100644
--- a/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs
+++ b/CollapseLauncher/Classes/CachesManagement/Honkai/Update.cs
@@ -126,12 +126,12 @@ private async Task UpdateCacheAsset((CacheAsset AssetIndex, IAssetProperty Asset
FileInfo fileInfo = new FileInfo(asset.AssetIndex.ConcatPath!)
.EnsureCreationOfDirectory()
- .EnsureNoReadOnly();
+ .EnsureNoReadOnly(out bool isExist);
// This is a action for Unused asset.
if (asset.AssetIndex.DataType == CacheAssetType.Unused)
{
- if (fileInfo.Exists)
+ if (isExist)
fileInfo.Delete();
LogWriteLine($"Deleted unused file: {fileInfo.FullName}", LogType.Default, true);
diff --git a/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs b/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs
index 1c02d0831..d4e71d8f0 100644
--- a/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs
+++ b/CollapseLauncher/Classes/CachesManagement/StarRail/Check.cs
@@ -1,4 +1,5 @@
-using Hi3Helper;
+using CollapseLauncher.Helper;
+using Hi3Helper;
using Hi3Helper.EncTool.Parser.AssetMetadata.SRMetadataAsset;
using System;
using System.Collections.Generic;
@@ -79,12 +80,12 @@ private async ValueTask CheckAsset(SRAsset asset, List returnAsset, str
}
// Get persistent and streaming paths
- FileInfo fileInfoPersistent = new FileInfo(Path.Combine(basePersistent!, asset.LocalName!));
- FileInfo fileInfoStreaming = new FileInfo(Path.Combine(baseStreaming!, asset.LocalName!));
+ FileInfo fileInfoPersistent = new FileInfo(Path.Combine(basePersistent!, asset.LocalName!)).EnsureNoReadOnly(out bool isPersistentExist);
+ FileInfo fileInfoStreaming = new FileInfo(Path.Combine(baseStreaming!, asset.LocalName!)).EnsureNoReadOnly(out bool isStreamingExist);
- bool UsePersistent = !fileInfoStreaming.Exists;
- bool IsPersistentExist = fileInfoPersistent.Exists && fileInfoPersistent.Length == asset.Size;
- bool IsStreamingExist = fileInfoStreaming.Exists && fileInfoStreaming.Length == asset.Size;
+ bool UsePersistent = !isStreamingExist;
+ bool IsPersistentExist = isPersistentExist && fileInfoPersistent.Length == asset.Size;
+ bool IsStreamingExist = isStreamingExist && fileInfoStreaming.Length == asset.Size;
asset.LocalName = UsePersistent ? fileInfoPersistent.FullName : fileInfoStreaming.FullName;
// Check if the file exist. If not, then add it to asset index.
diff --git a/CollapseLauncher/Classes/Extension/TaskExtensions.cs b/CollapseLauncher/Classes/Extension/TaskExtensions.cs
index 01ed2a487..fcac5708d 100644
--- a/CollapseLauncher/Classes/Extension/TaskExtensions.cs
+++ b/CollapseLauncher/Classes/Extension/TaskExtensions.cs
@@ -6,8 +6,8 @@
#nullable enable
namespace CollapseLauncher.Extension
{
- internal delegate Task ActionTimeoutValueTaskCallback(CancellationToken token);
- internal delegate void ActionOnTimeOutRetry(int retryAttemptCount, int retryAttemptTotal, int timeOutSecond, int timeOutStep);
+ public delegate Task ActionTimeoutValueTaskCallback(CancellationToken token);
+ public delegate void ActionOnTimeOutRetry(int retryAttemptCount, int retryAttemptTotal, int timeOutSecond, int timeOutStep);
internal static class TaskExtensions
{
internal const int DefaultTimeoutSec = 10;
diff --git a/CollapseLauncher/Classes/FileDialog/FileDialogHelper.cs b/CollapseLauncher/Classes/FileDialog/FileDialogHelper.cs
index 9a1f7652c..25271349f 100644
--- a/CollapseLauncher/Classes/FileDialog/FileDialogHelper.cs
+++ b/CollapseLauncher/Classes/FileDialog/FileDialogHelper.cs
@@ -162,7 +162,7 @@ private static bool IsCollapseProgramPath(ReadOnlySpan path)
///
/// Path to check
/// True if path is root of the drive
- internal static bool IsRootPath(ReadOnlySpan path)
+ public static bool IsRootPath(ReadOnlySpan path)
{
ReadOnlySpan rootPath = Path.GetPathRoot(path);
return rootPath.SequenceEqual(path);
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
index 7726b6a21..52227e874 100644
--- a/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
+++ b/CollapseLauncher/Classes/FileMigrationProcess/FileMigrationProcess.cs
@@ -90,20 +90,25 @@ private async Task StartRoutineInner(FileMigrationProcessUIRef uiRef)
private async Task MoveFile(FileMigrationProcessUIRef uiRef)
{
- FileInfo inputPathInfo = new FileInfo(this.inputPath!);
- FileInfo outputPathInfo = new FileInfo(this.outputPath!);
+ FileInfo inputPathInfo = new FileInfo(inputPath);
+ FileInfo outputPathInfo = new FileInfo(outputPath);
- string inputPathDir = Path.GetDirectoryName(inputPathInfo.FullName);
- string outputPathDir = Path.GetDirectoryName(outputPathInfo.FullName);
+ var inputPathDir = FileDialogHelper.IsRootPath(inputPath)
+ ? Path.GetPathRoot(inputPath)
+ : Path.GetDirectoryName(inputPathInfo.FullName);
- if (!Directory.Exists(outputPathDir))
- Directory.CreateDirectory(outputPathDir!);
+ if (string.IsNullOrEmpty(inputPathDir))
+ throw new InvalidOperationException(string.Format(Locale.Lang._Dialogs.InvalidGameDirNewTitleFormat,
+ inputPath));
+
+ DirectoryInfo outputPathDirInfo = new DirectoryInfo(inputPathDir);
+ outputPathDirInfo.Create();
// Update path display
string inputFileBasePath = inputPathInfo.FullName.Substring(inputPathDir!.Length + 1);
UpdateCountProcessed(uiRef, inputFileBasePath);
- if (this.IsSameOutputDrive)
+ if (IsSameOutputDrive)
{
Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file in the same drive from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
inputPathInfo.MoveTo(outputPathInfo.FullName, true);
@@ -112,7 +117,7 @@ private async Task MoveFile(FileMigrationProcessUIRef uiRef)
else
{
Logger.LogWriteLine($"[FileMigrationProcess::MoveFile()] Moving file across different drives from: {inputPathInfo.FullName} to {outputPathInfo.FullName}", LogType.Default, true);
- await MoveWriteFile(uiRef, inputPathInfo, outputPathInfo, this.tokenSource == null ? default : this.tokenSource.Token);
+ await MoveWriteFile(uiRef, inputPathInfo, outputPathInfo, tokenSource == null ? default : tokenSource.Token);
}
return outputPathInfo.FullName;
@@ -120,21 +125,19 @@ private async Task MoveFile(FileMigrationProcessUIRef uiRef)
private async Task MoveDirectory(FileMigrationProcessUIRef uiRef)
{
- DirectoryInfo inputPathInfo = new DirectoryInfo(this.inputPath!);
- if (!Directory.Exists(this.outputPath))
- Directory.CreateDirectory(this.outputPath!);
-
- DirectoryInfo outputPathInfo = new DirectoryInfo(this.outputPath);
+ DirectoryInfo inputPathInfo = new DirectoryInfo(inputPath);
+ DirectoryInfo outputPathInfo = new DirectoryInfo(outputPath);
+ outputPathInfo.Create();
int parentInputPathLength = inputPathInfo.Parent!.FullName.Length + 1;
string outputDirBaseNamePath = inputPathInfo.FullName.Substring(parentInputPathLength);
- string outputDirPath = Path.Combine(this.outputPath, outputDirBaseNamePath);
+ string outputDirPath = Path.Combine(outputPath, outputDirBaseNamePath);
await Parallel.ForEachAsync(
inputPathInfo.EnumerateFiles("*", SearchOption.AllDirectories),
new ParallelOptions
{
- CancellationToken = this.tokenSource?.Token ?? default,
+ CancellationToken = tokenSource?.Token ?? default,
MaxDegreeOfParallelism = LauncherConfig.AppCurrentThread
},
async (inputFileInfo, cancellationToken) =>
@@ -146,10 +149,14 @@ await Parallel.ForEachAsync(
UpdateCountProcessed(uiRef, inputFileBasePath);
string outputTargetPath = Path.Combine(outputPathInfo.FullName, inputFileBasePath);
- string outputTargetDirPath = Path.GetDirectoryName(outputTargetPath);
-
- if (!Directory.Exists(outputTargetDirPath))
- Directory.CreateDirectory(outputTargetDirPath!);
+ string outputTargetDirPath = Path.GetDirectoryName(outputTargetPath) ?? Path.GetPathRoot(outputTargetPath);
+
+ if (string.IsNullOrEmpty(outputTargetDirPath))
+ throw new InvalidOperationException(string.Format(Locale.Lang._Dialogs.InvalidGameDirNewTitleFormat,
+ inputPath));
+
+ DirectoryInfo outputTargetDirInfo = new DirectoryInfo(outputTargetDirPath);
+ outputTargetDirInfo.Create();
if (this.IsSameOutputDrive)
{
diff --git a/CollapseLauncher/Classes/FileMigrationProcess/IO.cs b/CollapseLauncher/Classes/FileMigrationProcess/IO.cs
index da4d21060..70a9fc411 100644
--- a/CollapseLauncher/Classes/FileMigrationProcess/IO.cs
+++ b/CollapseLauncher/Classes/FileMigrationProcess/IO.cs
@@ -17,13 +17,13 @@ private async Task MoveWriteFile(FileMigrationProcessUIRef uiRef, FileInfo input
{
int bufferSize = 1 << 18; // 256 kB Buffer
- if (inputFile!.Length < bufferSize)
+ if (inputFile.Length < bufferSize)
bufferSize = (int)inputFile.Length;
byte[] buffer = new byte[bufferSize];
await using (FileStream inputStream = inputFile.OpenRead())
- await using (FileStream outputStream = outputFile!.Exists && outputFile.Length <= inputFile.Length ?
+ await using (FileStream outputStream = outputFile.Exists && outputFile.Length <= inputFile.Length ?
outputFile.Open(FileMode.Open) : outputFile.Create())
{
// Set the output file size to inputStream's if the length is more than inputStream
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs
index 04317d8f7..2d795d371 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs
@@ -116,6 +116,11 @@ public bool UseCustomRegionBG
/// Determines if the game playtime should be synced to the database
///
public bool IsSyncPlaytimeToDatabase { get; set; } = true;
+
+ ///
+ /// Set per-region Discord Rich Presence setting for playing status
+ ///
+ public bool IsPlayingRpc { get; set; } = true;
#endregion
diff --git a/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs b/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs
index e89c4de1f..c17664892 100644
--- a/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs
+++ b/CollapseLauncher/Classes/Helper/Background/BackgroundMediaUtility.cs
@@ -66,7 +66,7 @@ internal enum MediaType
{
try
{
- await action.ConfigureAwait(false);
+ await action;
}
catch (Exception ex)
{
diff --git a/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs b/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs
index b2e9c9828..b7c89aa5b 100644
--- a/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs
+++ b/CollapseLauncher/Classes/Helper/HttpClientBuilder.cs
@@ -1,6 +1,7 @@
using CollapseLauncher.Helper.Update;
using Hi3Helper.Shared.Region;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
@@ -17,21 +18,22 @@ public class HttpClientBuilder : HttpClientBuilder;
private const int _maxConnectionsDefault = 32;
private const double _httpTimeoutDefault = 90; // in Seconds
- private bool IsUseProxy { get; set; } = true;
- private bool IsUseSystemProxy { get; set; } = true;
+ private bool IsUseProxy { get; set; } = true;
+ private bool IsUseSystemProxy { get; set; } = true;
private bool IsAllowHttpRedirections { get; set; }
- private bool IsAllowHttpCookies { get; set; }
- private bool IsAllowUntrustedCert { get; set; }
-
- private int MaxConnections { get; set; } = _maxConnectionsDefault;
- private DecompressionMethods DecompressionMethod { get; set; } = DecompressionMethods.All;
- private WebProxy? ExternalProxy { get; set; }
- private Version HttpProtocolVersion { get; set; } = HttpVersion.Version30;
- private string? HttpUserAgent { get; set; } = GetDefaultUserAgent();
- private string? HttpAuthHeader { get; set; }
- private HttpVersionPolicy HttpProtocolVersionPolicy { get; set; } = HttpVersionPolicy.RequestVersionOrLower;
- private TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(_httpTimeoutDefault);
- private Uri? HttpBaseUri { get; set; }
+ private bool IsAllowHttpCookies { get; set; }
+ private bool IsAllowUntrustedCert { get; set; }
+
+ private int MaxConnections { get; set; } = _maxConnectionsDefault;
+ private DecompressionMethods DecompressionMethod { get; set; } = DecompressionMethods.All;
+ private WebProxy? ExternalProxy { get; set; }
+ private Version HttpProtocolVersion { get; set; } = HttpVersion.Version30;
+ private string? HttpUserAgent { get; set; } = GetDefaultUserAgent();
+ private string? HttpAuthHeader { get; set; }
+ private HttpVersionPolicy HttpProtocolVersionPolicy { get; set; } = HttpVersionPolicy.RequestVersionOrLower;
+ private TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(_httpTimeoutDefault);
+ private Uri? HttpBaseUri { get; set; }
+ private Dictionary HttpHeaders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public HttpClientBuilder UseProxy(bool isUseSystemProxy = true)
{
@@ -191,6 +193,34 @@ public HttpClientBuilder SetBaseUrl(Uri baseUrl)
return this;
}
+ public HttpClientBuilder AddHeader(string key, string? value)
+ {
+ // Throw if the key is null or empty
+ ArgumentException.ThrowIfNullOrEmpty(key, nameof(key));
+
+ // Try check if the key is user-agent. If the user-agent has already
+ // been set, then override the value from HttpUserAgent property
+ if (key.Equals("User-Agent", StringComparison.OrdinalIgnoreCase))
+ {
+ HttpUserAgent = null;
+ }
+
+ // If the key already exist, then override the previous one.
+ // Otherwise, add the new key-value pair
+ // ReSharper disable once RedundantDictionaryContainsKeyBeforeAdding
+ if (HttpHeaders.ContainsKey(key))
+ {
+ HttpHeaders[key] = value;
+ }
+ else
+ {
+ HttpHeaders.Add(key, value);
+ }
+
+ // Return the instance of the builder
+ return this;
+ }
+
public HttpClient Create()
{
// Create the instance of the handler
@@ -273,6 +303,12 @@ public HttpClient Create()
if (!string.IsNullOrEmpty(HttpAuthHeader))
client.DefaultRequestHeaders.Add("Authorization", HttpAuthHeader);
+ // Add other headers
+ foreach (KeyValuePair header in HttpHeaders)
+ {
+ _ = client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
+ }
+
return client;
}
}
diff --git a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
index e559a9ff2..2a8b6933c 100644
--- a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
+++ b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
@@ -19,6 +19,7 @@
using System.Drawing;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
@@ -387,12 +388,22 @@ public static Bitmap Stream2Bitmap(IRandomAccessStream image)
/// FileInfo of the image to store
/// Is it hashed?
/// true if downloaded, false if not
- public static ValueTask DownloadAndEnsureCompleteness(FileInfo fileInfo, bool checkIsHashable)
+ public static Task IsFileCompletelyDownloadedAsync(FileInfo fileInfo, bool checkIsHashable)
{
// Check if the file exist
- return ValueTask.FromResult(IsFileCompletelyDownloaded(fileInfo, checkIsHashable));
+ return Task.Factory.StartNew(
+ () => IsFileCompletelyDownloaded(fileInfo, checkIsHashable),
+ CancellationToken.None,
+ TaskCreationOptions.DenyChildAttach,
+ TaskScheduler.Default);
}
+ ///
+ /// Check if background image is downloaded
+ ///
+ /// FileInfo of the image to store
+ /// Is it hashed?
+ /// true if downloaded, false if not
public static bool IsFileCompletelyDownloaded(FileInfo fileInfo, bool checkIsHashable)
{
// Get the parent path and file name
@@ -475,21 +486,25 @@ private static bool TryGetMd5HashFromFilename(string fileName, out byte[]? hash)
// Return true as it's a valid MD5 hash
return true;
}
-#nullable restore
private static HashSet _processingFiles = new();
private static HashSet _processingUrls = new();
-
- public static async void TryDownloadToCompletenessAsync(string url, FileInfo fileInfo, CancellationToken token)
- => await TryDownloadToCompleteness(url, fileInfo, token);
- public static async ValueTask TryDownloadToCompleteness(string url, FileInfo fileInfo, CancellationToken token)
+ public static async void TryDownloadToCompletenessDetached(string? url, HttpClient? useHttpClient, FileInfo fileInfo, CancellationToken token)
+ => _ = await TryDownloadToCompletenessAsync(url, useHttpClient, fileInfo, token);
+
+ public static async Task TryDownloadToCompletenessAsync(string? url, HttpClient? useHttpClient, FileInfo fileInfo, CancellationToken token)
{
+ if (string.IsNullOrEmpty(url))
+ {
+ return false;
+ }
+
if (_processingFiles.Contains(fileInfo) || _processingUrls.Contains(url))
{
Logger.LogWriteLine("Found duplicate download request, skipping...\r\n\t" +
$"URL : {url}", LogType.Warning, true);
- return;
+ return false;
}
byte[] buffer = ArrayPool.Shared.Rent(4 << 10);
try
@@ -505,49 +520,81 @@ public static async ValueTask TryDownloadToCompleteness(string url, FileInfo fil
if (fileInfo.Exists)
fileInfo.Delete();
- // Try to get the remote stream and download the file
- await using (Stream netStream = await FallbackCDNUtil.GetHttpStreamFromResponse(url, token))
+ int writeAttempt = 5;
+
+ while (writeAttempt > 0)
{
- await using (FileStream outStream = new FileStream(fileInfoTemp.FullName, FileMode.Create,
- FileAccess.ReadWrite, FileShare.ReadWrite))
+ // Try to get the remote stream and download the file
+ await using (Stream netStream = await GetFallbackStreamUrl(useHttpClient, url, token))
{
- // Get the file length
- fileLength = netStream.Length;
-
- // Create the prop file for download completeness checking
- string outputParentPath = Path.GetDirectoryName(fileInfoTemp.FullName);
- string outputFilename = Path.GetFileName(fileInfoTemp.FullName);
- if (outputParentPath != null)
+ await using (FileStream outStream = new FileStream(fileInfoTemp.FullName, FileMode.Create,
+ FileAccess.ReadWrite, FileShare.ReadWrite))
{
- string propFilePath = Path.Combine(outputParentPath, $"{outputFilename}#{netStream.Length}");
- await using (FileStream _ = new FileStream(propFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete))
+ // Get the file length
+ fileLength = netStream.Length;
+
+ // Create the prop file for download completeness checking
+ string? outputParentPath = Path.GetDirectoryName(fileInfoTemp.FullName);
+ string outputFilename = Path.GetFileName(fileInfoTemp.FullName);
+ if (outputParentPath != null)
{
- // Just create the file
+ string propFilePath = Path.Combine(outputParentPath, $"{outputFilename}#{netStream.Length}");
+ await using (FileStream _ = new FileStream(propFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete))
+ {
+ // Just create the file
+ }
}
+
+ // Copy (and download) the remote streams to local
+ int read;
+ while ((read = await netStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
+ await outStream.WriteAsync(buffer, 0, read, token);
}
+ }
+
+ if (await IsFileCompletelyDownloadedAsync(fileInfoTemp, true))
+ {
+ // Move to its original filename
+ fileInfoTemp.Refresh();
+ fileInfoTemp.MoveTo(fileInfo.FullName, true);
- // Copy (and download) the remote streams to local
- int read;
- while ((read = await netStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
- await outStream.WriteAsync(buffer, 0, read, token);
+ Logger.LogWriteLine($"Resource download from: {url} has been completed and stored locally into:"
+ + $"\"{fileInfo.FullName}\" with size: {ConverterTool.SummarizeSizeSimple(fileLength)} ({fileLength} bytes)", LogType.Default, true);
+
+ // Break from the loop and return true
+ return true;
}
- }
- // Move to its original filename
- fileInfoTemp.Refresh();
- fileInfoTemp.MoveTo(fileInfo.FullName, true);
+ Logger.LogWriteLine($"Failed to download resource from: {url} while trying to store into:"
+ + $"\"{fileInfo.FullName}\" with size: {ConverterTool.SummarizeSizeSimple(fileLength)} ({fileLength} bytes)"
+ + $". Remained attempt: {writeAttempt}", LogType.Warning, true);
- Logger.LogWriteLine($"Resource download from: {url} has been completed and stored locally into:"
- + $"\"{fileInfo.FullName}\" with size: {ConverterTool.SummarizeSizeSimple(fileLength)} ({fileLength} bytes)", LogType.Default, true);
+ // Decrement the write attempt
+ writeAttempt--;
+ }
+
+ // Throw as timeout
+ throw new TimeoutException($"The url: {url} keeps returning invalid data while trying to store into: {fileInfo.FullName}. Failing...");
}
// Ignore cancellation exceptions
- catch (TaskCanceledException) { }
- catch (OperationCanceledException) { }
+ catch (TaskCanceledException)
+ {
+ // Return false as Cancelled
+ return false;
+ }
+ catch (OperationCanceledException)
+ {
+ // Return false as Cancelled
+ return false;
+ }
catch (Exception ex)
{
// ErrorSender.SendException(ex, ErrorType.Connection);
await SentryHelper.ExceptionHandlerAsync(ex, SentryHelper.ExceptionType.UnhandledOther);
Logger.LogWriteLine($"Error has occured while downloading in background for: {url}\r\n{ex}", LogType.Error, true);
+
+ // Return false as failed
+ return false;
}
finally
{
@@ -557,7 +604,17 @@ public static async ValueTask TryDownloadToCompleteness(string url, FileInfo fil
}
}
- public static string GetCachedSprites(string URL, CancellationToken token)
+ private static async Task GetFallbackStreamUrl(HttpClient? client, string urlLocal, CancellationToken tokenLocal)
+ {
+ if (client == null)
+ return await FallbackCDNUtil.GetHttpStreamFromResponse(urlLocal, tokenLocal);
+
+ return await BridgedNetworkStream.CreateStream(
+ await client.GetAsync(urlLocal, HttpCompletionOption.ResponseHeadersRead, tokenLocal),
+ tokenLocal);
+ }
+
+ public static string? GetCachedSprites(HttpClient? httpClient, string? URL, CancellationToken token)
{
if (string.IsNullOrEmpty(URL)) return URL;
if (token.IsCancellationRequested) return URL;
@@ -572,12 +629,15 @@ public static string GetCachedSprites(string URL, CancellationToken token)
return cachePath;
}
- TryDownloadToCompletenessAsync(URL, fInfo, token);
+ TryDownloadToCompletenessDetached(URL, httpClient, fInfo, token);
return URL;
}
- public static async ValueTask GetCachedSpritesAsync(string URL, CancellationToken token)
+ public static async Task GetCachedSpritesAsync(string? URL, CancellationToken token)
+ => await GetCachedSpritesAsync(null, URL, token);
+
+ public static async Task GetCachedSpritesAsync(HttpClient? httpClient, string? URL, CancellationToken token)
{
if (string.IsNullOrEmpty(URL)) return URL;
@@ -586,9 +646,12 @@ public static async ValueTask GetCachedSpritesAsync(string URL, Cancella
Directory.CreateDirectory(AppGameImgCachedFolder);
FileInfo fInfo = new FileInfo(cachePath);
- if (!IsFileCompletelyDownloaded(fInfo, true))
+ if (!await IsFileCompletelyDownloadedAsync(fInfo, true))
{
- await TryDownloadToCompleteness(URL, fInfo, token);
+ if (!await TryDownloadToCompletenessAsync(URL, httpClient, fInfo, token))
+ {
+ return URL;
+ }
}
return cachePath;
}
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs
index 415725e82..2d08dba0e 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HoYoPlayLauncherApiLoader.cs
@@ -1,34 +1,78 @@
-#nullable enable
- using CollapseLauncher.Extension;
- using CollapseLauncher.Helper.LauncherApiLoader.Sophon;
- using CollapseLauncher.Helper.Metadata;
- using Hi3Helper;
- using System;
- using System.Collections.Generic;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
+using CollapseLauncher.Extension;
+using CollapseLauncher.Helper.LauncherApiLoader.Sophon;
+using CollapseLauncher.Helper.Metadata;
+using Hi3Helper;
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Threading;
+using System.Threading.Tasks;
// ReSharper disable IdentifierTypo
+// ReSharper disable once CheckNamespace
- // ReSharper disable once CheckNamespace
+#nullable enable
namespace CollapseLauncher.Helper.LauncherApiLoader.HoYoPlay
{
- internal class HoYoPlayLauncherApiLoader : LauncherApiBase, ILauncherApi
+ internal partial class HoYoPlayLauncherApiLoader : LauncherApiBase
{
+ private HttpClient? _apiGeneralHttpClient;
+ private HttpClient? _apiResourceHttpClient;
+
+ public sealed override HttpClient? ApiGeneralHttpClient { get => _apiGeneralHttpClient;
+ protected set => _apiGeneralHttpClient = value; }
+ public sealed override HttpClient? ApiResourceHttpClient { get => _apiResourceHttpClient;
+ protected set => _apiResourceHttpClient = value; }
+
private HoYoPlayLauncherApiLoader(PresetConfig presetConfig, string gameName, string gameRegion)
- : base(presetConfig, gameName, gameRegion) { }
+ : base(presetConfig, gameName, gameRegion, true)
+ {
+ // Set the HttpClientBuilder for HoYoPlay's own General API.
+ HttpClientBuilder apiGeneralHttpBuilder = new HttpClientBuilder()
+ .UseLauncherConfig()
+ .AllowUntrustedCert()
+ .SetHttpVersion(HttpVersion.Version30)
+ .SetAllowedDecompression()
+ .AddHeader("x-rpc-device_id", GetDeviceId(presetConfig));
+
+ // Set the HttpClientBuilder for HoYoPlay's own Resource API.
+ HttpClientBuilder apiResourceHttpBuilder = new HttpClientBuilder()
+ .UseLauncherConfig()
+ .AllowUntrustedCert()
+ .SetHttpVersion(HttpVersion.Version30)
+ .SetAllowedDecompression(DecompressionMethods.None)
+ .AddHeader("x-rpc-device_id", GetDeviceId(presetConfig));
+
+ // If the metadata has user-agent defined, set the resource's HttpClient user-agent
+ if (!string.IsNullOrEmpty(presetConfig.ApiGeneralUserAgent))
+ {
+ apiGeneralHttpBuilder.SetUserAgent(presetConfig.ApiGeneralUserAgent);
+ }
+ if (!string.IsNullOrEmpty(presetConfig.ApiResourceUserAgent))
+ {
+ apiResourceHttpBuilder.SetUserAgent(string.Format(presetConfig.ApiResourceUserAgent, InnerLauncherConfig.m_isWindows11 ? "11" : "10"));
+ }
+
+ // Add other API general and resource headers from the metadata configuration
+ presetConfig.AddApiGeneralAdditionalHeaders((key, value) => apiGeneralHttpBuilder.AddHeader(key, value));
+ presetConfig.AddApiResourceAdditionalHeaders((key, value) => apiResourceHttpBuilder.AddHeader(key, value));
+
+ // Create HttpClient instances for both General and Resource APIs.
+ ApiGeneralHttpClient = apiGeneralHttpBuilder.Create();
+ ApiResourceHttpClient = apiResourceHttpBuilder.Create();
+ }
public static ILauncherApi CreateApiInstance(PresetConfig presetConfig, string gameName, string gameRegion)
=> new HoYoPlayLauncherApiLoader(presetConfig, gameName, gameRegion);
protected override async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onTimeoutRoutine, CancellationToken token)
{
- EnsurePresetConfigNotNull();
-
ActionTimeoutValueTaskCallback hypResourceResponseCallback =
- async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken);
+ async innerToken => await ApiGeneralHttpClient!.GetFromJsonAsync(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken);
// Assign as 3 Task array
Task[] tasks = [
@@ -49,7 +93,7 @@ protected override async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onT
{
ActionTimeoutValueTaskCallback hypPluginResourceCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherPluginURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken);
+ await ApiGeneralHttpClient!.GetFromJsonAsync(PresetConfig?.LauncherPluginURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken);
tasks[1] = hypPluginResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep,
ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypPluginResource = result);
@@ -59,7 +103,7 @@ protected override async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onT
{
ActionTimeoutValueTaskCallback hypSdkResourceCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherGameChannelSDKURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken);
+ await ApiGeneralHttpClient!.GetFromJsonAsync(PresetConfig?.LauncherGameChannelSDKURL, InternalAppJSONContext.Default.HoYoPlayLauncherResources, innerToken);
tasks[2] = hypSdkResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep,
ExecutionTimeoutAttempt, onTimeoutRoutine, token).AsTaskAndDoAction((result) => hypSdkResource = result);
@@ -78,7 +122,7 @@ protected override async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onT
};
// Await all callbacks
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ await Task.WhenAll(tasks);
ConvertPluginResources(ref sophonResourceData, hypPluginResource);
ConvertSdkResources(ref sophonResourceData, hypSdkResource);
@@ -333,11 +377,11 @@ protected override async Task LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRo
ActionTimeoutValueTaskCallback hypLauncherBackgroundCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(launcherSpriteUrl, InternalAppJSONContext.Default.HoYoPlayLauncherNews, innerToken);
+ await ApiResourceHttpClient!.GetFromJsonAsync(launcherSpriteUrl, InternalAppJSONContext.Default.HoYoPlayLauncherNews, innerToken);
ActionTimeoutValueTaskCallback hypLauncherNewsCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(launcherNewsUrl, InternalAppJSONContext.Default.HoYoPlayLauncherNews, innerToken);
+ await ApiResourceHttpClient!.GetFromJsonAsync(launcherNewsUrl, InternalAppJSONContext.Default.HoYoPlayLauncherNews, innerToken);
HoYoPlayLauncherNews? hypLauncherBackground = null;
HoYoPlayLauncherNews? hypLauncherNews = null;
@@ -364,7 +408,7 @@ protected override async Task LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRo
ConvertLauncherSocialMedia(ref sophonLauncherNewsRoot, hypLauncherNews?.Data);
base.LauncherGameNews = sophonLauncherNewsRoot;
- base.LauncherGameNews?.Content?.InjectDownloadableItemCancelToken(token);
+ base.LauncherGameNews?.Content?.InjectDownloadableItemCancelToken(this, token);
}
#region Convert Launcher News
@@ -485,7 +529,7 @@ protected override async Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeo
ActionTimeoutValueTaskCallback hypLauncherGameInfoCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(launcherGameInfoUrl, InternalAppJSONContext.Default.HoYoPlayLauncherGameInfo, innerToken);
+ await ApiResourceHttpClient!.GetFromJsonAsync(launcherGameInfoUrl, InternalAppJSONContext.Default.HoYoPlayLauncherGameInfo, innerToken);
HoYoPlayLauncherGameInfo? hypLauncherGameInfo = await hypLauncherGameInfoCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep,
ExecutionTimeoutAttempt, onTimeoutRoutine, token);
@@ -515,5 +559,85 @@ private void ConvertGameInfoResources([DisallowNull] ref HoYoPlayGameInfoField?
sophonGameInfo = hypLauncherGameInfoList.Data?.FirstOrDefault(x => x.BizName?.Equals(PresetConfig?.LauncherBizName) ?? false);
}
#endregion
+
+ #region GetDeviceId override
+ protected sealed override string GetDeviceId(PresetConfig preset)
+ {
+ // Determine if the client is a mainland client based on the zone name
+ bool isMainlandClient = (preset.ZoneName?.Equals("Mainland China") ?? false) || (preset.ZoneName?.Equals("Bilibili") ?? false);
+
+ // Set the publisher name based on the client type
+ string publisherName = isMainlandClient ? "miHoYo" : "Cognosphere";
+ // Define the registry root path for the publisher
+ string registryRootPath = $@"Software\{publisherName}\HYP";
+
+ // Open the registry key for the root path
+ RegistryKey? rootRegistryKey = Registry.CurrentUser.OpenSubKey(registryRootPath, true);
+ // Find or create the HYP device ID
+ string hypDeviceId = FindOrCreateHYPDeviceId(rootRegistryKey, isMainlandClient, registryRootPath);
+ return hypDeviceId;
+ }
+
+ private string FindOrCreateHYPDeviceId(RegistryKey? rootRegistryKey, bool isMainlandClient, string registryRootPath)
+ {
+ // Define default version keys for mainland and global clients
+ const string HYPVerDefaultCN = "1_1";
+ const string HYPVerDefaultGlb = "1_0";
+
+ // Use the root registry key or create it if it doesn't exist
+ using (rootRegistryKey ??= Registry.CurrentUser.CreateSubKey(registryRootPath, true))
+ {
+ // Get the subkey names under the root registry key
+ string[] subKeyNames = rootRegistryKey.GetSubKeyNames();
+ foreach (string subKeyNameString in subKeyNames)
+ {
+ // Open each subkey and check for the HYPDeviceId value
+ using RegistryKey? subKeyNameKey = rootRegistryKey.OpenSubKey(subKeyNameString, true);
+ if (subKeyNameKey == null)
+ {
+ continue;
+ }
+
+ // Get the current HYP device ID from the subkey
+ string? currentHypDeviceId = (string?)subKeyNameKey.GetValue("HYPDeviceId", null);
+ if (string.IsNullOrEmpty(currentHypDeviceId))
+ {
+ continue;
+ }
+
+ // Return the current HYP device ID if found
+ return currentHypDeviceId;
+ }
+
+ // Open or create the subkey for the default version based on the client type
+ using RegistryKey subRegistryKey = rootRegistryKey.OpenSubKey(isMainlandClient ? HYPVerDefaultCN : HYPVerDefaultGlb, true)
+ ?? rootRegistryKey.CreateSubKey(isMainlandClient ? HYPVerDefaultCN : HYPVerDefaultGlb, true);
+
+ // Generate a new HYP device ID
+ string newHypDeviceId = CreateNewDeviceId();
+ // Set the new HYP device ID in the subkey
+ subRegistryKey.SetValue("HYPDeviceId", newHypDeviceId, RegistryValueKind.String);
+
+ return newHypDeviceId;
+ }
+ }
+
+ private string CreateNewDeviceId()
+ {
+ // Define the registry key path for cryptography settings
+ const string regKeyCryptography = @"SOFTWARE\Microsoft\Cryptography";
+
+ // Open the registry key for reading
+ using RegistryKey? rootRegistryKey = Registry.LocalMachine.OpenSubKey(regKeyCryptography, true);
+ // Retrieve the MachineGuid value from the registry, or generate a new GUID if it doesn't exist
+ string guid = ((string?)rootRegistryKey?.GetValue("MachineGuid", null) ??
+ Guid.NewGuid().ToString()).Replace("-", string.Empty);
+
+ // Append the current Unix timestamp in milliseconds to the GUID
+ string guidWithEpochMs = guid + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ // Return the combined GUID and timestamp
+ return guidWithEpochMs;
+ }
+ #endregion
}
}
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/ILauncherApi.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/ILauncherApi.cs
index be23dcc2a..db21df9f1 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/ILauncherApi.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/ILauncherApi.cs
@@ -1,13 +1,15 @@
using CollapseLauncher.Extension;
using CollapseLauncher.Helper.LauncherApiLoader.HoYoPlay;
using CollapseLauncher.Helper.LauncherApiLoader.Sophon;
+using System;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace CollapseLauncher.Helper.LauncherApiLoader
{
- internal interface ILauncherApi
+ public interface ILauncherApi : IDisposable
{
bool IsLoadingCompleted { get; }
string? GameBackgroundImg { get; }
@@ -19,6 +21,8 @@ internal interface ILauncherApi
HoYoPlayGameInfoField? LauncherGameInfoField { get; }
RegionResourceProp? LauncherGameResource { get; }
LauncherGameNews? LauncherGameNews { get; }
+ HttpClient? ApiGeneralHttpClient { get; }
+ HttpClient? ApiResourceHttpClient { get; }
Task LoadAsync(OnLoadAction? beforeLoadRoutine = null, OnLoadAction? afterLoadRoutine = null,
ActionOnTimeOutRetry? onTimeoutRoutine = null, ErrorLoadRoutineDelegate? errorLoadRoutine = null,
CancellationToken token = default);
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs
index 652698f17..19cea540a 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/LauncherApiBase.cs
@@ -11,15 +11,18 @@
using System.Threading;
using System.Threading.Tasks;
using Hi3Helper.SentryHelper;
+using System.Net.Http;
+using System.Net;
+using System.Net.Http.Json;
#nullable enable
namespace CollapseLauncher.Helper.LauncherApiLoader
{
- internal delegate void OnLoadAction(CancellationToken token);
+ public delegate void OnLoadAction(CancellationToken token);
- internal delegate void ErrorLoadRoutineDelegate(Exception ex);
+ public delegate void ErrorLoadRoutineDelegate(Exception ex);
- internal class LauncherApiBase
+ internal partial class LauncherApiBase : ILauncherApi
{
public const int ExecutionTimeout = 10;
public const int ExecutionTimeoutStep = 5;
@@ -41,12 +44,71 @@ internal class LauncherApiBase
public virtual RegionResourceProp? LauncherGameResource { get; protected set; }
public virtual LauncherGameNews? LauncherGameNews { get; protected set; }
public virtual HoYoPlayGameInfoField? LauncherGameInfoField { get; protected set; }
+ public virtual HttpClient? ApiGeneralHttpClient { get => field; protected set => field = value; }
+ public virtual HttpClient? ApiResourceHttpClient { get => field; protected set => field = value; }
+
+ public void Dispose()
+ {
+ ApiGeneralHttpClient?.Dispose();
+ ApiResourceHttpClient?.Dispose();
+ }
+
+ ~LauncherApiBase()
+ {
+ Dispose();
+ }
protected LauncherApiBase(PresetConfig presetConfig, string gameName, string gameRegion)
+ : this(presetConfig, gameName, gameRegion, false) { }
+
+ protected LauncherApiBase(PresetConfig presetConfig, string gameName, string gameRegion, bool isIgnoreBaseHttpClientInit)
{
PresetConfig = presetConfig;
GameName = gameName;
GameRegion = gameRegion;
+
+ EnsurePresetConfigNotNull();
+
+ if (!isIgnoreBaseHttpClientInit)
+ {
+ InitializeHttpClients(presetConfig);
+ }
+ }
+
+ private void InitializeHttpClients(PresetConfig presetConfig)
+ {
+ // Create generic HttpClientBuilder
+ HttpClientBuilder apiGeneralHttpBuilder = new HttpClientBuilder()
+ .UseLauncherConfig()
+ .AllowUntrustedCert()
+ .SetAllowedDecompression()
+ .SetHttpVersion(HttpVersion.Version30);
+
+ // Create resource HttpClientBuilder
+ HttpClientBuilder apiResourceHttpBuilder = new HttpClientBuilder()
+ .UseLauncherConfig()
+ .AllowUntrustedCert()
+ .SetAllowedDecompression(DecompressionMethods.None)
+ .SetHttpVersion(HttpVersion.Version30);
+
+ // If the metadata has user-agent defined, set the resource's HttpClient user-agent
+ if (!string.IsNullOrEmpty(presetConfig.ApiGeneralUserAgent))
+ {
+ apiGeneralHttpBuilder.SetUserAgent(presetConfig.ApiGeneralUserAgent);
+ }
+ if (!string.IsNullOrEmpty(presetConfig.ApiResourceUserAgent))
+ {
+ apiResourceHttpBuilder.SetUserAgent(string.Format(presetConfig.ApiResourceUserAgent, InnerLauncherConfig.m_isWindows11 ? "11" : "10"));
+ }
+
+ // Add other API general and resource headers from the metadata configuration
+ presetConfig.AddApiGeneralAdditionalHeaders((key, value) => apiGeneralHttpBuilder.AddHeader(key, value));
+ presetConfig.AddApiResourceAdditionalHeaders((key, value) => apiResourceHttpBuilder.AddHeader(key, value));
+
+ // Create HttpClient instances for both General and Resource APIs.
+ ApiGeneralHttpClient = apiGeneralHttpBuilder.Create();
+ ApiResourceHttpClient = apiResourceHttpBuilder.Create();
+
}
public async Task LoadAsync(OnLoadAction? beforeLoadRoutine, OnLoadAction? afterLoadRoutine,
@@ -82,7 +144,7 @@ await Task.WhenAll([
LoadLauncherGameResource(onTimeoutRoutine, token),
LoadLauncherNews(onTimeoutRoutine, token),
LoadLauncherGameInfo(onTimeoutRoutine, token)
- ]).ConfigureAwait(false);
+ ]);
}
protected virtual async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onTimeoutRoutine,
@@ -93,7 +155,7 @@ protected virtual async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onTi
ActionTimeoutValueTaskCallback launcherGameResourceCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default.RegionResourceProp, innerToken);
+ await ApiGeneralHttpClient!.GetFromJsonAsync(PresetConfig?.LauncherResourceURL, InternalAppJSONContext.Default.RegionResourceProp, innerToken);
Task[] tasks = [
launcherGameResourceCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep,
@@ -107,14 +169,14 @@ protected virtual async Task LoadLauncherGameResource(ActionOnTimeOutRetry? onTi
{
ActionTimeoutValueTaskCallback launcherPluginPropCallback =
async innerToken =>
- await FallbackCDNUtil.DownloadAsJSONType(string.Format(PresetConfig?.LauncherPluginURL!, GetDeviceId(PresetConfig!)), InternalAppJSONContext.Default.RegionResourceProp, innerToken);
+ await ApiGeneralHttpClient!.GetFromJsonAsync(string.Format(PresetConfig?.LauncherPluginURL!, GetDeviceId(PresetConfig!)), InternalAppJSONContext.Default.RegionResourceProp, innerToken);
tasks[1] = launcherPluginPropCallback.WaitForRetryAsync(ExecutionTimeout, ExecutionTimeoutStep,
ExecutionTimeoutAttempt, onTimeoutRoutine, token)
.AsTaskAndDoAction((result) => pluginProp = result);
}
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ await Task.WhenAll(tasks);
if (LauncherGameResource == null)
{
@@ -258,7 +320,7 @@ protected virtual async Task LoadLauncherNews(ActionOnTimeOutRetry? onTimeoutRou
await LoadLauncherNewsInner(true, localeFallback, PresetConfig, onTimeoutRoutine, token);
}
- regionResourceProp?.Content?.InjectDownloadableItemCancelToken(token);
+ regionResourceProp?.Content?.InjectDownloadableItemCancelToken(this, token);
LauncherGameNews = regionResourceProp;
}
@@ -271,7 +333,7 @@ protected virtual async Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeou
LauncherGameInfoField = new HoYoPlayGameInfoField();
}
- private static async ValueTask LoadLauncherNewsInner(
+ private async ValueTask LoadLauncherNewsInner(
bool isMultiLang, string lang, PresetConfig presetConfig, ActionOnTimeOutRetry? onTimeoutRoutine,
CancellationToken token)
{
@@ -291,18 +353,16 @@ protected virtual async Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeou
onTimeoutRoutine, token);
}
- private static async Task LoadSingleLangLauncherNews(
+ private async Task LoadSingleLangLauncherNews(
string launcherSpriteUrl, CancellationToken token)
{
- return await FallbackCDNUtil.DownloadAsJSONType(launcherSpriteUrl, InternalAppJSONContext.Default.LauncherGameNews, token);
+ return await ApiResourceHttpClient!.GetFromJsonAsync(launcherSpriteUrl, InternalAppJSONContext.Default.LauncherGameNews, token);
}
- private static async Task LoadMultiLangLauncherNews(string launcherSpriteUrl, string lang,
+ private async Task LoadMultiLangLauncherNews(string launcherSpriteUrl, string lang,
CancellationToken token)
{
- return await
- FallbackCDNUtil
- .DownloadAsJSONType(string.Format(launcherSpriteUrl, lang), InternalAppJSONContext.Default.LauncherGameNews, token);
+ return await ApiResourceHttpClient!.GetFromJsonAsync(string.Format(launcherSpriteUrl, lang), InternalAppJSONContext.Default.LauncherGameNews, token);
}
protected virtual string GetDeviceId(PresetConfig preset)
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs
index 59ac9600b..f537e85a6 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/Sophon/LauncherGameNews.cs
@@ -4,6 +4,7 @@
using CollapseLauncher.Helper.LauncherApiLoader.HoYoPlay;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Text.Json.Serialization;
using System.Threading;
using WinRT;
@@ -30,6 +31,7 @@ public enum LauncherGameAvailabilityStatus
public interface ILauncherGameNewsDataTokenized
{
public CancellationToken? InnerToken { get; set; }
+ public ILauncherApi? LauncherApi { get; set; }
}
public class LauncherGameNews
@@ -137,13 +139,13 @@ public List? NewsPostTypeAnnouncement
}
}
- public void InjectDownloadableItemCancelToken(CancellationToken token)
+ public void InjectDownloadableItemCancelToken(ILauncherApi? launcherApi, CancellationToken token)
{
- InjectDownloadableItemCancelTokenInner(NewsCarousel, token);
- InjectDownloadableItemCancelTokenInner(SocialMedia, token);
+ InjectDownloadableItemCancelTokenInner(NewsCarousel, launcherApi, token);
+ InjectDownloadableItemCancelTokenInner(SocialMedia, launcherApi, token);
}
- private void InjectDownloadableItemCancelTokenInner(IEnumerable? prop, CancellationToken token)
+ private void InjectDownloadableItemCancelTokenInner(IEnumerable? prop, ILauncherApi? launcherApi, CancellationToken token)
where T : ILauncherGameNewsDataTokenized
{
if (prop == null)
@@ -153,11 +155,11 @@ private void InjectDownloadableItemCancelTokenInner(IEnumerable? prop, Can
foreach (T? propValue in prop)
{
- InjectDownloadableItemCancelTokenInner(propValue, token);
+ InjectDownloadableItemCancelTokenInner(propValue, launcherApi, token);
}
}
- private static void InjectDownloadableItemCancelTokenInner(T? prop, CancellationToken token)
+ private static void InjectDownloadableItemCancelTokenInner(T? prop, ILauncherApi? launcherApi, CancellationToken token)
where T : ILauncherGameNewsDataTokenized
{
if (prop == null)
@@ -165,6 +167,7 @@ private static void InjectDownloadableItemCancelTokenInner(T? prop, Cancellat
return;
}
+ prop.LauncherApi = launcherApi;
prop.InnerToken = token;
}
}
@@ -191,7 +194,23 @@ public class LauncherGameNewsBackground
public string? FeaturedEventIconBtnUrl { get; set; }
}
- public class LauncherGameNewsCarousel : ILauncherGameNewsDataTokenized
+ public class LauncherUIResourceBase
+ {
+ public LauncherUIResourceBase()
+ { }
+
+ public LauncherUIResourceBase(ILauncherApi? launcherApi)
+ {
+ LauncherApi = launcherApi;
+ }
+
+ public ILauncherApi? LauncherApi { get; set; }
+
+ private HttpClient? _httpClient;
+ public HttpClient? CurrentHttpClient { get => _httpClient ??= LauncherApi?.ApiResourceHttpClient; }
+ }
+
+ public class LauncherGameNewsCarousel : LauncherUIResourceBase, ILauncherGameNewsDataTokenized
{
private readonly string? _carouselImg;
@@ -207,7 +226,7 @@ public class LauncherGameNewsCarousel : ILauncherGameNewsDataTokenized
[JsonConverter(typeof(SanitizeUrlStringConverter))]
public string? CarouselImg
{
- get => ImageLoaderHelper.GetCachedSprites(_carouselImg, InnerToken ?? default);
+ get => ImageLoaderHelper.GetCachedSprites(CurrentHttpClient, _carouselImg, InnerToken ?? default);
init => _carouselImg = value;
}
@@ -223,7 +242,7 @@ public string? CarouselImg
}
[GeneratedBindableCustomProperty]
- public partial class LauncherGameNewsSocialMedia : ILauncherGameNewsDataTokenized
+ public partial class LauncherGameNewsSocialMedia : LauncherUIResourceBase, ILauncherGameNewsDataTokenized
{
private readonly string? _qrImg;
private readonly List? _qrLinks;
@@ -240,7 +259,7 @@ public partial class LauncherGameNewsSocialMedia : ILauncherGameNewsDataTokenize
[JsonConverter(typeof(SanitizeUrlStringConverter))]
public string? IconImg
{
- get => ImageLoaderHelper.GetCachedSprites(_iconImg, InnerToken ?? default);
+ get => ImageLoaderHelper.GetCachedSprites(CurrentHttpClient, _iconImg, InnerToken ?? default);
init => _iconImg = value;
}
@@ -248,7 +267,7 @@ public string? IconImg
[JsonConverter(typeof(SanitizeUrlStringConverter))]
public string? IconImgHover
{
- get => ImageLoaderHelper.GetCachedSprites(_iconImgHover, InnerToken ?? default);
+ get => ImageLoaderHelper.GetCachedSprites(CurrentHttpClient, _iconImgHover, InnerToken ?? default);
init => _iconImgHover = value;
}
@@ -275,7 +294,7 @@ public string? Title
[JsonConverter(typeof(SanitizeUrlStringConverter))]
public string? QrImg
{
- get => ImageLoaderHelper.GetCachedSprites(_qrImg, InnerToken ?? default);
+ get => ImageLoaderHelper.GetCachedSprites(CurrentHttpClient, _qrImg, InnerToken ?? default);
init => _qrImg = value;
}
diff --git a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs
index 0d5484f84..394c61281 100644
--- a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs
+++ b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs
@@ -879,5 +879,31 @@ private bool CheckInnerGameConfig(string gamePath, LauncherType launcherType)
}
#endregion
+
+ #region API General and Resource Headers
+ public string? ApiResourceUserAgent { get; set; }
+ public string? ApiGeneralUserAgent { get; set; }
+
+ public Dictionary? ApiGeneralAdditionalHeaders { get; set; }
+ public Dictionary? ApiResourceAdditionalHeaders { get; set; }
+
+ public void AddApiGeneralAdditionalHeaders(Action addHandler)
+ => AddAdditionalHeadersFromDict(ApiGeneralAdditionalHeaders, addHandler);
+
+ public void AddApiResourceAdditionalHeaders(Action addHandler)
+ => AddAdditionalHeadersFromDict(ApiResourceAdditionalHeaders, addHandler);
+
+ private void AddAdditionalHeadersFromDict(Dictionary? dict, Action addHandler)
+ {
+ if (dict == null || dict.Count == 0)
+ {
+ return;
+ }
+ foreach (KeyValuePair header in dict)
+ {
+ addHandler(header.Key, header.Value);
+ }
+ }
+ #endregion
}
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs
index a32aba646..aae99a6b7 100644
--- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs
+++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.PkgVersion.cs
@@ -98,6 +98,11 @@ internal partial class InstallManagerBase
{
[StringSyntax("Regex")]
protected const string NonGameFileRegexPattern = @"(\.\d\d\d|(zip|7z)|patch)|\.$";
+
+ [GeneratedRegex(NonGameFileRegexPattern, RegexOptions.NonBacktracking)]
+ private static partial Regex GetNonGameFileRegex();
+ private static Regex NonGameFileRegex = GetNonGameFileRegex();
+
public virtual async ValueTask CleanUpGameFiles(bool withDialog = true)
{
// Get the unused file info asynchronously
@@ -452,11 +457,7 @@ protected virtual bool IsCategorizedAsGameFile(FileInfo fileInfo, string gamePat
}
// 8th check: Ensure that the file is one of package files
- if (includeZipCheck && Regex.IsMatch(fileName,
- NonGameFileRegexPattern,
- RegexOptions.Compiled |
- RegexOptions.NonBacktracking
- ))
+ if (includeZipCheck && NonGameFileRegex.IsMatch(fileName))
{
return true;
}
diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.Sophon.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.Sophon.cs
new file mode 100644
index 000000000..d738b424d
--- /dev/null
+++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.Sophon.cs
@@ -0,0 +1,937 @@
+// ReSharper disable IdentifierTypo
+// ReSharper disable StringLiteralTypo
+// ReSharper disable ForCanBeConvertedToForeach
+// ReSharper disable IdentifierTypo
+// ReSharper disable CommentTypo
+// ReSharper disable InconsistentNaming
+// ReSharper disable GrammarMistakeInComment
+
+using CollapseLauncher.Dialogs;
+using CollapseLauncher.Helper;
+using Hi3Helper;
+using Hi3Helper.Data;
+using Hi3Helper.SentryHelper;
+using Hi3Helper.Shared.ClassStruct;
+using Hi3Helper.Shared.Region;
+using Hi3Helper.Sophon;
+using Hi3Helper.Sophon.Structs;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Numerics;
+using System.Threading;
+using System.Threading.Tasks;
+using SophonLogger = Hi3Helper.Sophon.Helper.Logger;
+
+#nullable enable
+namespace CollapseLauncher.InstallManager.Base
+{
+ internal delegate ValueTask SignedValueTaskSelectorAsync(
+ TFrom item, CancellationToken ctx)
+ where TResult : struct, ISignedNumber;
+
+ internal partial class InstallManagerBase
+ {
+ #region Protected Virtual Properties
+ protected virtual string _gameSophonChunkDir => Path.Combine(_gamePath, "chunk_collapse");
+ #endregion
+
+ #region Protected Properties
+ protected List _sophonVOLanguageList { get; set; } = [];
+ protected bool _isSophonDownloadCompleted { get; set; }
+ protected bool _isSophonPreloadCompleted
+ {
+ get => File.Exists(Path.Combine(_gameSophonChunkDir, PreloadVerifiedFileName));
+ set
+ {
+ string verifiedFile =
+ EnsureCreationOfDirectory(Path.Combine(_gameSophonChunkDir, PreloadVerifiedFileName));
+ try
+ {
+ FileInfo fileInfo = new FileInfo(verifiedFile);
+ if (value)
+ {
+ fileInfo.Create().Dispose();
+ return;
+ }
+
+ if (fileInfo.Exists)
+ {
+ fileInfo.IsReadOnly = false;
+ fileInfo.Delete();
+ }
+ }
+ catch (Exception ex)
+ {
+ SentryHelper.ExceptionHandler(ex, SentryHelper.ExceptionType.UnhandledOther);
+ Logger.LogWriteLine($"Error while deleting/creating sophon preload completion file! {ex}",
+ LogType.Warning, true);
+ }
+ }
+ }
+ #endregion
+
+ #region Public Virtual Properties
+ public virtual bool IsUseSophon =>
+ _gameVersionManager.GamePreset.LauncherResourceChunksURL != null
+ && !File.Exists(Path.Combine(_gamePath, "@DisableSophon"))
+ && !_canDeltaPatch && !_forceIgnoreDeltaPatch
+ && LauncherConfig.GetAppConfigValue("IsEnableSophon").ToBool();
+ public virtual bool IsSophonInUpdateMode => _isSophonInUpdateMode;
+ #endregion
+
+ #region Sophon Verification Methods
+ protected virtual void CleanupTempSophonVerifiedFiles()
+ {
+ DirectoryInfo dirPath = new(_gameSophonChunkDir);
+ try
+ {
+ if (!dirPath.Exists)
+ {
+ return;
+ }
+
+ foreach (FileInfo file in dirPath.EnumerateFiles("*.verified", SearchOption.TopDirectoryOnly)
+ .EnumerateNoReadOnly())
+ {
+ file.Delete();
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+ #endregion
+
+ #region Sophon Download and Install/Update/Preload Methods
+ public virtual async Task StartPackageInstallSophon(GameInstallStateEnum gameState)
+ {
+ // Set the flag to false
+ _isSophonDownloadCompleted = false;
+
+ // Set the max thread and httpHandler based on settings
+ int maxThread = SophonGetThreadNum();
+ int maxChunksThread = Math.Clamp(maxThread / 2, 2, 32);
+ int maxHttpHandler = Math.Max(maxThread, SophonGetHttpHandler());
+
+ Logger.LogWriteLine($"Initializing Sophon Chunk download method with Main Thread: {maxThread}, Chunks Thread: {maxChunksThread} and Max HTTP handle: {maxHttpHandler}",
+ LogType.Default, true);
+
+ // Initialize the HTTP client
+ HttpClient httpClient = new HttpClientBuilder()
+ .UseLauncherConfig(maxHttpHandler)
+ .Create();
+
+ using (ThreadPoolThrottle.Start())
+ {
+ // Create a sophon download speed limiter instance
+ SophonDownloadSpeedLimiter downloadSpeedLimiter =
+ SophonDownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached);
+
+ // Reset status and progress properties
+ ResetStatusAndProgress();
+
+ // Set the progress bar to indetermined
+ _isSophonInUpdateMode = false;
+ _status.IsIncludePerFileIndicator = false;
+ _status.IsProgressPerFileIndetermined = true;
+ _status.IsProgressAllIndetermined = true;
+ UpdateStatus();
+
+ // Clear the VO language list
+ _sophonVOLanguageList.Clear();
+
+ // Subscribe the logger event
+ SophonLogger.LogHandler += UpdateSophonLogHandler;
+
+ // Get the requested URL and version based on current state.
+ if (_gameVersionManager.GamePreset
+ .LauncherResourceChunksURL != null)
+ {
+ // 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);
+ }
+
+ #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
+
+ // 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);
+
+ // Ensure that the manifest is ordered based on _gameVoiceLanguageLocaleIdOrdered
+ RearrangeSophonDataLocaleOrder(sophonMainInfoPair.OtherSophonData);
+
+ // Add the manifest to the pair list
+ sophonInfoPairList.Add(sophonMainInfoPair);
+
+ List voLanguageList =
+ GetSophonLanguageDisplayDictFromVoicePackList(sophonMainInfoPair.OtherSophonData);
+
+ // Get Audio Choices first
+ (List addedVo, int setAsDefaultVo) =
+ await SimpleDialogs.Dialog_ChooseAudioLanguageChoice(
+ voLanguageList,
+ GetSophonLocaleCodeIndex(
+ sophonMainInfoPair.OtherSophonData,
+ "ja-jp"
+ ));
+
+ try
+ {
+ 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
+ UpdateStatus();
+
+ // Get game install path and create directory if not exist
+ string gameInstallPath = _gamePath;
+ if (!string.IsNullOrEmpty(gameInstallPath))
+ {
+ Directory.CreateDirectory(gameInstallPath);
+ }
+
+ // Get the list of the Sophon Assets first
+ List sophonAssetList = await GetSophonAssetListFromPair(
+ httpClient,
+ sophonInfoPairList,
+ downloadSpeedLimiter,
+ _token.Token);
+
+ // Check for the disk space requirement first and ensure that the space is sufficient
+ await EnsureDiskSpaceSufficiencyAsync(
+ _progressAllSizeTotal,
+ gameInstallPath,
+ sophonAssetList,
+ async (sophonAsset, ctx) =>
+ {
+ return await Task.Factory.StartNew(() =>
+ {
+ // Get the file path and start the write process
+ string assetName = sophonAsset.AssetName;
+ string assetFullPath = Path.Combine(gameInstallPath, assetName);
+ long sophonAssetLen = sophonAsset.AssetSize;
+ FileInfo filePath = new FileInfo(assetFullPath + "_tempSophon");
+ FileInfo origFilePath = new FileInfo(assetFullPath);
+
+ // If the original file path exist and the length is the same as the asset size
+ // (means the file has already been downloaded, then return 0)
+ if (origFilePath.Exists && origFilePath.Length == sophonAssetLen)
+ {
+ return 0L;
+ }
+
+ // If the temp file path exist and the length is the same as the asset size
+ // (means the file has already been downloaded, then return 0)
+ if (filePath.Exists && filePath.Length == sophonAssetLen)
+ {
+ return 0L;
+ }
+
+ // If both orig and temp file don't exist or has different size, then return the asset size
+ return sophonAsset.AssetSize;
+ }, ctx,
+ TaskCreationOptions.DenyChildAttach,
+ TaskScheduler.Default);
+ }, _token.Token);
+
+ // Get the parallel options
+ var parallelOptions = new ParallelOptions
+ {
+ MaxDegreeOfParallelism = maxThread,
+ CancellationToken = _token.Token
+ };
+ var parallelChunksOptions = new ParallelOptions
+ {
+ MaxDegreeOfParallelism = maxChunksThread,
+ CancellationToken = _token.Token
+ };
+
+ // Set the progress bar to indetermined
+ _status.IsIncludePerFileIndicator = false;
+ _status.IsProgressPerFileIndetermined = false;
+ _status.IsProgressAllIndetermined = false;
+ UpdateStatus();
+
+ // Enumerate the asset in parallel and start the download process
+ await Parallel.ForEachAsync(sophonAssetList, parallelOptions, DelegateAssetDownload)
+ .ConfigureAwait(false);
+
+ // Rename temporary files
+ await Parallel.ForEachAsync(sophonAssetList, parallelOptions, DelegateAssetRenameTempFile)
+ .ConfigureAwait(false);
+
+ // Remove sophon verified files
+ CleanupTempSophonVerifiedFiles();
+
+ // Declare the download delegate
+ ValueTask DelegateAssetDownload(SophonAsset asset, CancellationToken _)
+ // ReSharper disable once AccessToDisposedClosure
+ {
+ return 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;
+ }
+
+ // Throw if the token cancellation requested
+ token.ThrowIfCancellationRequested();
+
+ // Get the file path and start the write process
+ string assetName = asset.AssetName;
+ string assetFullPath = Path.Combine(gameInstallPath, assetName);
+ FileInfo filePath = new FileInfo(assetFullPath + "_tempSophon")
+ .EnsureCreationOfDirectory()
+ .EnsureNoReadOnly();
+ FileInfo origFilePath = new FileInfo(assetFullPath)
+ .EnsureNoReadOnly(out bool isExist);
+
+ if (!isExist)
+ {
+ return;
+ }
+
+ filePath.MoveTo(origFilePath.FullName, true);
+ filePath.Refresh();
+ origFilePath.Refresh();
+ }, token);
+ }
+
+ _isSophonDownloadCompleted = true;
+ }
+ finally
+ {
+ // Unsubscribe the logger event
+ SophonLogger.LogHandler -= UpdateSophonLogHandler;
+ httpClient.Dispose();
+
+ // Unsubscribe download limiter
+ LauncherConfig.DownloadSpeedLimitChanged -= downloadSpeedLimiter.GetListener();
+ }
+ }
+ }
+ }
+
+ private async Task> GetSophonAssetListFromPair(
+ HttpClient client,
+ IEnumerable sophonInfoPairs,
+ SophonDownloadSpeedLimiter downloadSpeedLimiter,
+ CancellationToken token)
+ {
+ List sophonAssetList = [];
+
+ // Avoid duplicates by using HashSet of the url
+ HashSet currentlyProcessedPair = [];
+ foreach (SophonChunkManifestInfoPair sophonDownloadInfoPair in sophonInfoPairs)
+ {
+ // Try add and if the hashset already contains the same Manifest ID registered, then skip
+ if (!currentlyProcessedPair.Add(sophonDownloadInfoPair.ManifestInfo.ManifestId))
+ {
+ Logger.LogWriteLine($"Found duplicate operation for {sophonDownloadInfoPair.ManifestInfo.ManifestId}! Skipping...",
+ LogType.Warning, true);
+ continue;
+ }
+
+ // Register to hashset to avoid duplication
+ // Enumerate the pair to get the SophonAsset
+ await foreach (SophonAsset sophonAsset in SophonManifest.EnumerateAsync(
+ client,
+ sophonDownloadInfoPair,
+ downloadSpeedLimiter,
+ token))
+ {
+ // If the asset is a directory, skip
+ if (sophonAsset.IsDirectory)
+ {
+ continue;
+ }
+
+ sophonAssetList.Add(sophonAsset);
+ }
+ }
+
+ // Return the list
+ return sophonAssetList;
+ }
+
+
+ public virtual async Task StartPackageUpdateSophon(GameInstallStateEnum gameState, bool isPreloadMode)
+ {
+ // Set the flag to false
+ _isSophonDownloadCompleted = false;
+
+ // Set the max thread and httpHandler based on settings
+ int maxThread = SophonGetThreadNum();
+ int maxChunksThread = Math.Clamp(maxThread / 2, 2, 32);
+ int maxHttpHandler = Math.Max(maxThread, SophonGetHttpHandler());
+
+ Logger.LogWriteLine($"Initializing Sophon Chunk update method with Main Thread: {maxThread}, Chunks Thread: {maxChunksThread} and Max HTTP handle: {maxHttpHandler}",
+ LogType.Default, true);
+
+ // Initialize the HTTP client
+ HttpClient httpClient = new HttpClientBuilder()
+ .UseLauncherConfig(maxHttpHandler)
+ .Create();
+
+ try
+ {
+ // Reset status and progress properties
+ ResetStatusAndProgress();
+
+ // Set the progress bar to indetermined
+ _isSophonInUpdateMode = !isPreloadMode;
+ _status.IsIncludePerFileIndicator = !isPreloadMode;
+ _status.IsProgressPerFileIndetermined = true;
+ _status.IsProgressAllIndetermined = true;
+ UpdateStatus();
+
+ // Clear the VO language list
+ _sophonVOLanguageList.Clear();
+
+ // Subscribe the logger event
+ SophonLogger.LogHandler += UpdateSophonLogHandler;
+
+ // Init asset list
+ List sophonUpdateAssetList = [];
+
+ // Get the previous version details of the preload or the recent update.
+ GameVersion? requestedVersionFrom = _gameVersionManager!.GetGameExistingVersion();
+ if (_gameVersionManager.GamePreset.LauncherResourceChunksURL != null)
+ {
+ // 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);
+ }
+
+ string? requestedBaseUrlFrom = isPreloadMode
+ ? _gameVersionManager.GamePreset.LauncherResourceChunksURL.PreloadUrl
+ : _gameVersionManager.GamePreset.LauncherResourceChunksURL.MainUrl;
+ #if SIMULATEAPPLYPRELOAD
+ string requestedBaseUrlTo = _gameVersionManager.GamePreset.LauncherResourceChunksURL.PreloadUrl!;
+ #else
+ string requestedBaseUrlTo = requestedBaseUrlFrom!;
+ #endif
+ // Add the tag query to the previous version's Url
+ requestedBaseUrlFrom += $"&tag={requestedVersionFrom.ToString()}";
+
+ // Create a sophon download speed limiter instance
+ SophonDownloadSpeedLimiter downloadSpeedLimiter =
+ SophonDownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached);
+
+ // Add base game diff data
+ await AddSophonDiffAssetsToList(httpClient, requestedBaseUrlFrom, requestedBaseUrlTo,
+ sophonUpdateAssetList, "game", downloadSpeedLimiter);
+
+ // If the game has lang list path, then add it
+ if (_gameAudioLangListPath != null)
+ {
+ // Add existing voice-over diff data
+ await AddSophonAdditionalVODiffAssetsToList(httpClient, requestedBaseUrlFrom,
+ requestedBaseUrlTo, sophonUpdateAssetList,
+ downloadSpeedLimiter);
+ }
+ }
+
+ // Get the remote chunk size
+ _progressPerFileSizeTotal = sophonUpdateAssetList.GetCalculatedDiffSize(!isPreloadMode);
+ _progressPerFileSizeCurrent = 0;
+
+ // Get the remote total size and current total size
+ _progressAllCountTotal = sophonUpdateAssetList.Count(x => !x.IsDirectory);
+ _progressAllSizeTotal = !isPreloadMode
+ ? sophonUpdateAssetList.Sum(x => x.AssetSize)
+ : _progressPerFileSizeTotal;
+ _progressAllSizeCurrent = 0;
+
+ // Get the parallel options
+ var parallelOptions = new ParallelOptions
+ {
+ MaxDegreeOfParallelism = maxThread,
+ CancellationToken = _token.Token
+ };
+ var parallelChunksOptions = new ParallelOptions
+ {
+ MaxDegreeOfParallelism = maxChunksThread,
+ CancellationToken = _token.Token
+ };
+
+ // Get the update source and destination, also where the staging chunk files will be stored
+ string chunkPath = _gameSophonChunkDir;
+ string gamePath = _gamePath;
+
+ // If the chunk directory is not exist, then create one.
+ if (!string.IsNullOrEmpty(chunkPath))
+ {
+ Directory.CreateDirectory(chunkPath);
+ }
+
+ bool canDeleteChunks = _canDeleteZip;
+ bool isSophonPreloadCompleted = _isSophonPreloadCompleted;
+
+ // If preload completed and perf mode is on, use all CPU cores
+ if (isSophonPreloadCompleted && LauncherConfig.GetAppConfigValue("SophonPreloadApplyPerfMode").ToBool())
+ {
+ maxThread = Environment.ProcessorCount;
+ maxChunksThread = Math.Clamp(maxThread / 2, 2, 32);
+
+ parallelOptions = new ParallelOptions
+ {
+ MaxDegreeOfParallelism = maxThread,
+ CancellationToken = _token.Token
+ };
+ parallelChunksOptions = new ParallelOptions
+ {
+ MaxDegreeOfParallelism = maxChunksThread,
+ CancellationToken = _token.Token
+ };
+ }
+
+ // Test the disk space requirement first and ensure that the space is sufficient
+ await EnsureDiskSpaceSufficiencyAsync(
+ _progressPerFileSizeTotal,
+ chunkPath,
+ sophonUpdateAssetList,
+ async (x, ctx) =>
+ await x.GetDownloadedPreloadSize(
+ chunkPath,
+ gamePath,
+ isPreloadMode,
+ ctx),
+ _token.Token);
+
+ _status.IsProgressPerFileIndetermined = false;
+ _status.IsProgressAllIndetermined = false;
+ _status.ActivityStatus = $"{(_isSophonInUpdateMode && !isPreloadMode
+ ? Locale.Lang._Misc.UpdatingAndApplying
+ : Locale.Lang._Misc.Downloading)}: {string.Format(Locale.Lang._Misc.PerFromTo, _progressAllCountCurrent,
+ _progressAllCountTotal)}";
+ UpdateStatus();
+
+ var processingAsset = new ConcurrentDictionary();
+
+ // Set the delegate function for the download action
+ async ValueTask Action(HttpClient localHttpClient, SophonAsset asset)
+ {
+ if (!processingAsset.TryAdd(asset, 0))
+ {
+ Logger.LogWriteLine($"Found duplicate operation for {asset.AssetName}! Skipping...",
+ LogType.Warning, true);
+ return;
+ }
+
+ if (isPreloadMode)
+ {
+ // If preload mode, then only download the chunks
+ await asset.DownloadDiffChunksAsync(localHttpClient, chunkPath, parallelChunksOptions,
+ UpdateSophonFileTotalProgress, null,
+ UpdateSophonDownloadStatus, isSophonPreloadCompleted);
+ return;
+ }
+
+ // Ensure to remove the read-only attribute
+ string currentAssetPath = Path.Combine(gamePath, asset.AssetName);
+ TryUnassignReadOnlyFileSingle(currentAssetPath);
+
+ // Otherwise, start the patching process
+ await asset.WriteUpdateAsync(localHttpClient, gamePath, gamePath, chunkPath, canDeleteChunks,
+ parallelChunksOptions, UpdateSophonFileTotalProgress,
+ UpdateSophonFileDownloadProgress, UpdateSophonDownloadStatus);
+ processingAsset.Remove(asset, out _);
+ }
+
+ // Enumerate in parallel and process the assets
+ await Parallel.ForEachAsync(sophonUpdateAssetList.Where(x => !x.IsDirectory),
+ parallelOptions,
+ (asset, _) => Action(httpClient, asset));
+
+ _isSophonPreloadCompleted = isPreloadMode;
+
+ // If it's in update mode, then clean up the temp sophon verified files
+ if (!isPreloadMode)
+ {
+ CleanupTempSophonVerifiedFiles();
+ }
+
+ _isSophonDownloadCompleted = true;
+ }
+ finally
+ {
+ // Unsubscribe the logger event
+ SophonLogger.LogHandler -= UpdateSophonLogHandler;
+ httpClient.Dispose();
+
+ // Check if the DXSETUP file is exist, then delete it.
+ // The DXSETUP files causes some false positive detection of data modification
+ // for some games (like Genshin, which causes 4302-x errors for some reason)
+ string dxSetupDir = Path.Combine(_gamePath, "DXSETUP");
+ TryDeleteReadOnlyDir(dxSetupDir);
+ }
+ }
+
+ private ValueTask RunSophonAssetDownloadThread(HttpClient client, SophonAsset asset,
+ ParallelOptions parallelOptions)
+ {
+ // If the asset is a dictionary, then return
+ if (asset.IsDirectory)
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ // Get the file path and start the write process
+ var assetName = asset.AssetName;
+ var filePath = EnsureCreationOfDirectory(Path.Combine(_gamePath, assetName));
+
+ // Get the target and temp file info
+ FileInfo existingFileInfo = new FileInfo(filePath).EnsureNoReadOnly(out bool isExistingFileInfoExist);
+ FileInfo sophonFileInfo =
+ new FileInfo(filePath + "_tempSophon").EnsureNoReadOnly(out bool isSophonFileInfoExist);
+
+ // Use "_tempSophon" if file is new or if "_tempSophon" file exist. Otherwise use original file if exist
+ if (!isExistingFileInfoExist || isSophonFileInfoExist
+ || (isExistingFileInfoExist && isSophonFileInfoExist))
+ {
+ filePath = sophonFileInfo.FullName;
+ }
+
+ // However if the file has already been existed and completely downloaded while _tempSophon is exist,
+ // delete the _tempSophon one to avoid uncompleted files being applied instead.
+ else if (isExistingFileInfoExist && existingFileInfo.Length == asset.AssetSize && isSophonFileInfoExist)
+ {
+ sophonFileInfo.Delete();
+ }
+
+ return asset.WriteToStreamAsync(
+ client,
+ () => new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite,
+ FileShare.ReadWrite),
+ parallelOptions,
+ UpdateSophonFileTotalProgress,
+ UpdateSophonFileDownloadProgress,
+ UpdateSophonDownloadStatus
+ );
+ }
+
+ private async Task EnsureDiskSpaceSufficiencyAsync(
+ long sizeToCompare,
+ string gamePath,
+ List assetList,
+ SignedValueTaskSelectorAsync sizeSelector,
+ CancellationToken token)
+ {
+ // Get SIMD'ed total sizes
+ long downloadedSize = await assetList.SumParallelAsync(
+ async (x, ctx) => await sizeSelector(x, ctx),
+ token);
+
+ long sizeRemainedToDownload = sizeToCompare - downloadedSize;
+
+ // Push log regarding size
+ Logger.LogWriteLine($"Total free space required to download: {ConverterTool.SummarizeSizeSimple(sizeToCompare)}"
+ + $" and {ConverterTool.SummarizeSizeSimple(sizeRemainedToDownload)} remained to be downloaded.",
+ LogType.Default, true);
+
+ // Get the information about the disk
+ DriveInfo driveInfo = new DriveInfo(gamePath);
+
+ // Push log regarding disk space
+ Logger.LogWriteLine($"Total free space remained on disk: {driveInfo.Name}: {ConverterTool.SummarizeSizeSimple(driveInfo.TotalFreeSpace)}.",
+ LogType.Default, true);
+
+ // If the space is insufficient, then show the dialog and throw
+ if (sizeRemainedToDownload > driveInfo.TotalFreeSpace)
+ {
+ string errStr = $"Free Space on {driveInfo.Name} is not sufficient! " +
+ $"(Free space: {ConverterTool.SummarizeSizeSimple(driveInfo.TotalFreeSpace)}, Req. Space: {ConverterTool.SummarizeSizeSimple(sizeRemainedToDownload)} (Total: {ConverterTool.SummarizeSizeSimple(sizeToCompare)}), " +
+ $"Drive: {driveInfo.Name})";
+ await SimpleDialogs.Dialog_InsufficientDriveSpace(_parentUI, driveInfo.TotalFreeSpace,
+ sizeRemainedToDownload, driveInfo.Name);
+
+ // Push log for the disk space error
+ Logger.LogWriteLine(errStr, LogType.Error, true);
+ throw new TaskCanceledException(errStr);
+ }
+ }
+
+ #endregion
+
+ #region Sophon Asset Package Methods
+
+ private async Task AddSophonDiffAssetsToList(HttpClient httpClient,
+ string requestedUrlFrom,
+ string requestedUrlTo,
+ List sophonPreloadAssetList,
+ string matchingField,
+ SophonDownloadSpeedLimiter downloadSpeedLimiter)
+ {
+ // Get the manifest pair for both previous (from) and next (to) version
+ SophonChunkManifestInfoPair requestPairFrom = await SophonManifest
+ .CreateSophonChunkManifestInfoPair(httpClient, requestedUrlFrom, matchingField, _token.Token);
+ SophonChunkManifestInfoPair requestPairTo = await SophonManifest
+ .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
+ .EnumerateUpdateAsync(httpClient, requestPairFrom, requestPairTo,
+ false, downloadSpeedLimiter)
+ .WithCancellation(_token.Token))
+ {
+ sophonPreloadAssetList.Add(sophonAsset);
+ }
+ }
+
+ private async Task AddSophonAdditionalVODiffAssetsToList(HttpClient httpClient,
+ string requestedUrlFrom,
+ string requestedUrlTo,
+ List sophonPreloadAssetList,
+ SophonDownloadSpeedLimiter downloadSpeedLimiter)
+ {
+ // Get the main VO language name from Id
+ string mainLangId = GetLanguageLocaleCodeByID(_gameVoiceLanguageID);
+ // Get the manifest pair for both previous (from) and next (to) version for the main VO
+ await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo,
+ sophonPreloadAssetList, mainLangId, downloadSpeedLimiter);
+
+ // Check if the audio lang list file is exist, then try add others
+ FileInfo fileInfo = new FileInfo(_gameAudioLangListPath).EnsureNoReadOnly();
+ if (fileInfo.Exists)
+ {
+ // Use stream reader to read the list one-by-one
+ using StreamReader reader = new StreamReader(_gameAudioLangListPath);
+ // Read until EOF
+ while (await reader.ReadLineAsync() is { } line)
+ {
+ // Get other lang Id, pass it and try add to the list
+ string? otherLangId = GetLanguageLocaleCodeByLanguageString(line
+ #if !DEBUG
+ , false
+ #endif
+ );
+
+ // Check if the voice pack is actually the same as default.
+ if (string.IsNullOrEmpty(otherLangId) ||
+ otherLangId.Equals(mainLangId, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // Get the manifest pair for both previous (from) and next (to) version for other VOs
+ await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo,
+ sophonPreloadAssetList, otherLangId, downloadSpeedLimiter);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Sophon Thread Related Methods
+
+ protected virtual int SophonGetThreadNum()
+ {
+ // Get from config
+ var n = LauncherConfig.GetAppConfigValue("SophonCpuThread").ToInt();
+ if (n == 0) // If config is default "0", then use sqrt of thread number as safe number
+ {
+ n = (int)Math.Sqrt(Environment.ProcessorCount);
+ }
+
+ return Math.Clamp(n, 2, 64); // Clamp value to prevent errors
+ }
+
+ protected virtual int SophonGetHttpHandler()
+ {
+ var n = LauncherConfig.GetAppConfigValue("SophonHttpConnInt").ToInt();
+ if (n == 0)
+ {
+ n = (int)Math.Sqrt(Environment.ProcessorCount) * 2;
+ }
+
+ return Math.Clamp(n, 4, 128);
+ }
+
+ #endregion
+
+ #region Sophon Audio/Voice-Packs Locale Methods
+
+ protected virtual int GetSophonLocaleCodeIndex(SophonData sophonData, string lookupName)
+ {
+ List localeList = sophonData.ManifestIdentityList
+ .Where(x => IsValidLocaleCode(x.MatchingField))
+ .Select(x => x.MatchingField.ToLower())
+ .ToList();
+
+ int index = localeList.IndexOf(lookupName);
+ return Math.Max(0, index);
+ }
+
+ protected virtual List GetSophonLanguageDisplayDictFromVoicePackList(SophonData sophonData)
+ {
+ var value = new List();
+ foreach (SophonManifestIdentity identity in sophonData.ManifestIdentityList)
+ {
+ // Check the lang ID and add the translation of the language to the list
+ string localeCode = identity.MatchingField.ToLower();
+ if (IsValidLocaleCode(localeCode))
+ {
+ string? languageDisplay = GetLanguageDisplayByLocaleCode(localeCode, false);
+ if (string.IsNullOrEmpty(languageDisplay))
+ {
+ continue;
+ }
+
+ value.Add(languageDisplay);
+ }
+ }
+
+ return value;
+ }
+
+ 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 WriteAudioLangListSophon(List sophonVOList)
+ {
+ // Create persistent directory if not exist
+ if (!Directory.Exists(_gameDataPersistentPath))
+ {
+ Directory.CreateDirectory(_gameDataPersistentPath);
+ }
+
+ // If the game does not have audio lang list, then return
+ if (string.IsNullOrEmpty(_gameAudioLangListPathStatic))
+ {
+ return;
+ }
+
+ // Read all the existing list
+ List langList = File.Exists(_gameAudioLangListPathStatic)
+ ? File.ReadAllLines(_gameAudioLangListPathStatic).ToList()
+ : [];
+
+ // Try lookup if there is a new language list, then add it to the list
+ for (int index = 0; index < sophonVOList.Count; index++)
+ {
+ var packageLocaleCodeString = sophonVOList[index];
+ string langString = GetLanguageStringByLocaleCode(packageLocaleCodeString);
+ if (!langList.Contains(langString, StringComparer.OrdinalIgnoreCase))
+ {
+ langList.Add(langString);
+ }
+ }
+
+ // Create the audio lang list file
+ using var sw = new StreamWriter(_gameAudioLangListPathStatic,
+ new FileStreamOptions
+ { Mode = FileMode.Create, Access = FileAccess.Write });
+ // Iterate the package list
+ foreach (var voIds in langList)
+ // Write the language string as per ID
+ {
+ sw.WriteLine(voIds);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs
index 729bc0dc5..5d394b30e 100644
--- a/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs
+++ b/CollapseLauncher/Classes/InstallManagement/BaseClass/InstallManagerBase.cs
@@ -30,9 +30,6 @@
using Hi3Helper.SentryHelper;
using Hi3Helper.Shared.ClassStruct;
using Hi3Helper.Shared.Region;
-using Hi3Helper.Sophon;
-using Hi3Helper.Sophon.Infos;
-using Hi3Helper.Sophon.Structs;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -41,7 +38,6 @@
using SharpHDiffPatch.Core;
using SharpHDiffPatch.Core.Event;
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -65,9 +61,6 @@
using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry;
#endif
-using SophonLogger = Hi3Helper.Sophon.Helper.Logger;
-using SophonManifest = Hi3Helper.Sophon.SophonManifest;
-
// ReSharper disable ForCanBeConvertedToForeach
// ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault
// ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
@@ -136,7 +129,6 @@ protected struct UninstallGameProperty
$"{Path.GetFileNameWithoutExtension(_gameVersionManager.GamePreset.GameExecutableName)}_Data");
protected virtual string _gameDataPersistentPath => Path.Combine(_gameDataPath, "Persistent");
- protected virtual string _gameSophonChunkDir => Path.Combine(_gamePath, "chunk_collapse");
protected virtual string _gameAudioLangListPath => null;
protected virtual string _gameAudioLangListPathStatic => null;
protected IRepair _gameRepairTool { get; set; }
@@ -154,40 +146,6 @@ protected struct UninstallGameProperty
private long _totalLastSizeCurrent;
protected bool _isAllowExtractCorruptZip { get; set; }
- protected bool _isSophonDownloadCompleted { get; set; }
-
- protected bool _isSophonPreloadCompleted
- {
- get => File.Exists(Path.Combine(_gameSophonChunkDir, PreloadVerifiedFileName));
- set
- {
- string verifiedFile =
- EnsureCreationOfDirectory(Path.Combine(_gameSophonChunkDir, PreloadVerifiedFileName));
- try
- {
- FileInfo fileInfo = new FileInfo(verifiedFile);
- if (value)
- {
- fileInfo.Create().Dispose();
- return;
- }
-
- if (fileInfo.Exists)
- {
- fileInfo.IsReadOnly = false;
- fileInfo.Delete();
- }
- }
- catch (Exception ex)
- {
- SentryHelper.ExceptionHandler(ex, SentryHelper.ExceptionType.UnhandledOther);
- LogWriteLine($"Error while deleting/creating sophon preload completion file! {ex}", LogType.Warning,
- true);
- }
- }
- }
-
- protected List _sophonVOLanguageList { get; set; } = [];
protected UninstallGameProperty? _uninstallGameProperty { get; set; }
#endregion
@@ -197,38 +155,6 @@ protected bool _isSophonPreloadCompleted
public event EventHandler FlushingTrigger;
public virtual bool StartAfterInstall { get; set; }
public virtual bool IsRunning { get; protected set; }
-
- public virtual bool IsUseSophon =>
- _gameVersionManager.GamePreset.LauncherResourceChunksURL != null
- && !File.Exists(Path.Combine(_gamePath, "@DisableSophon"))
- && !_canDeltaPatch && !_forceIgnoreDeltaPatch
- && LauncherConfig.GetAppConfigValue("IsEnableSophon").ToBool();
-
- protected virtual int SophonGetThreadNum()
- {
- // Get from config
- var n = LauncherConfig.GetAppConfigValue("SophonCpuThread").ToInt();
- if (n == 0) // If config is default "0", then use sqrt of thread number as safe number
- {
- n = (int)Math.Sqrt(Environment.ProcessorCount);
- }
-
- return Math.Clamp(n, 2, 64); // Clamp value to prevent errors
- }
-
- protected virtual int SophonGetHttpHandler()
- {
- var n = LauncherConfig.GetAppConfigValue("SophonHttpConnInt").ToInt();
- if (n == 0)
- {
- n = (int)Math.Sqrt(Environment.ProcessorCount) * 2;
- }
-
- return Math.Clamp(n, 4, 128);
- }
-
- public virtual bool IsSophonInUpdateMode => _isSophonInUpdateMode;
-
#endregion
public InstallManagerBase(UIElement parentUI, IGameVersionCheck GameVersionManager)
@@ -242,16 +168,6 @@ public InstallManagerBase(UIElement parentUI, IGameVersionCheck GameVersionManag
UpdateCompletenessStatus(CompletenessStatus.Idle);
}
- /*
- ~InstallManagerBase()
- {
-#if DEBUG
- LogWriteLine($"[~InstallManagerBase()] Deconstructor getting called in {_gameVersionManager}", LogType.Warning, true);
-#endif
- Dispose();
- }
- */
-
protected void ResetToken()
{
_token = new CancellationTokenSourceWrapper();
@@ -795,613 +711,6 @@ public virtual async ValueTask StartPackageVerification(List _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();
-
- // 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);
-
- // Ensure that the manifest is ordered based on _gameVoiceLanguageLocaleIdOrdered
- RearrangeSophonDataLocaleOrder(sophonMainInfoPair.OtherSophonData);
-
- // Add the manifest to the pair list
- sophonInfoPairList.Add(sophonMainInfoPair);
-
- List voLanguageList =
- GetSophonLanguageDisplayDictFromVoicePackList(sophonMainInfoPair.OtherSophonData);
-
- // Get Audio Choices first
- (List addedVO, int setAsDefaultVO) =
- await Dialog_ChooseAudioLanguageChoice(voLanguageList, GetSophonLocaleCodeIndex(sophonMainInfoPair.OtherSophonData, "ja-jp"));
-
- try
- {
- 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 (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 sophonInfoPairListLocal,
- ParallelOptions parallelOptions,
- Func actionDelegate)
- {
- // Create a sophon download speed limiter instance
- SophonDownloadSpeedLimiter downloadSpeedLimiter = SophonDownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached);
-
- try
- {
- LauncherConfig.DownloadSpeedLimitChanged += downloadSpeedLimiter.GetListener();
- var processingInfoPair = new ConcurrentDictionary();
- var infoPairListCopy = sophonInfoPairListLocal.ToList();
- foreach (SophonChunkManifestInfoPair sophonDownloadInfoPair in infoPairListCopy)
- {
- if (!processingInfoPair.TryAdd(sophonDownloadInfoPair.ChunksInfo, 0))
- {
- LogWriteLine($"Found duplicate operation for {sophonDownloadInfoPair.ChunksInfo.ChunksBaseUrl}! Skipping...",
- LogType.Warning, true);
- continue;
- }
- // Enumerate in parallel and process the assets
- await Parallel.ForEachAsync(SophonManifest.EnumerateAsync(client, sophonDownloadInfoPair,
- downloadSpeedLimiter),
- parallelOptions,
- actionDelegate).ConfigureAwait(false);
- processingInfoPair.Remove(sophonDownloadInfoPair.ChunksInfo, out _);
- }
- }
- finally
- {
- LauncherConfig.DownloadSpeedLimitChanged -= downloadSpeedLimiter.GetListener();
- }
- }
- }
-
- public virtual async Task StartPackageUpdateSophon(GameInstallStateEnum gameState, bool isPreloadMode)
- {
- // Set the flag to false
- _isSophonDownloadCompleted = false;
-
- // Set the max thread and httpHandler based on settings
- int maxThread = SophonGetThreadNum();
- int maxChunksThread = Math.Clamp(maxThread / 2, 2, 32);
- int maxHttpHandler = Math.Max(maxThread, SophonGetHttpHandler());
-
- LogWriteLine($"Initializing Sophon Chunk update method with Main Thread: {maxThread}, Chunks Thread: {maxChunksThread} and Max HTTP handle: {maxHttpHandler}",
- LogType.Default, true);
-
- // Initialize the HTTP client
- HttpClient httpClient = new HttpClientBuilder()
- .UseLauncherConfig(maxHttpHandler)
- .Create();
-
- try
- {
- // Reset status and progress properties
- ResetStatusAndProgress();
-
- // Clear the VO language list
- _sophonVOLanguageList?.Clear();
-
- // Subscribe the logger event
- SophonLogger.LogHandler += UpdateSophonLogHandler;
-
- // Init asset list
- List sophonUpdateAssetList = [];
-
- // Get the previous version details of the preload or the recent update.
- GameVersion? requestedVersionFrom = _gameVersionManager!.GetGameExistingVersion();
- 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))
- {
- await _gameVersionManager.GamePreset
- .LauncherResourceChunksURL
- .EnsureReassociated(
- httpClient,
- branchUrl,
- _gameVersionManager.GamePreset.LauncherBizName,
- _token.Token);
- }
- #nullable restore
-
- string requestedBaseUrlFrom = isPreloadMode
- ? _gameVersionManager.GamePreset.LauncherResourceChunksURL.PreloadUrl
- : _gameVersionManager.GamePreset.LauncherResourceChunksURL.MainUrl;
- #if SIMULATEAPPLYPRELOAD
- string requestedBaseUrlTo = _gameVersionManager.GamePreset.LauncherResourceChunksURL.PreloadUrl;
- #else
- string requestedBaseUrlTo = requestedBaseUrlFrom;
- #endif
- // Add the tag query to the previous version's Url
- requestedBaseUrlFrom += $"&tag={requestedVersionFrom.ToString()}";
-
- // Create a sophon download speed limiter instance
- SophonDownloadSpeedLimiter downloadSpeedLimiter = SophonDownloadSpeedLimiter.CreateInstance(LauncherConfig.DownloadSpeedLimitCached);
-
- // Add base game diff data
- await AddSophonDiffAssetsToList(httpClient, requestedBaseUrlFrom, requestedBaseUrlTo,
- sophonUpdateAssetList, "game", downloadSpeedLimiter);
-
- // If the game has lang list path, then add it
- if (_gameAudioLangListPath != null)
- {
- // Add existing voice-over diff data
- await AddSophonAdditionalVODiffAssetsToList(httpClient, requestedBaseUrlFrom,
- requestedBaseUrlTo, sophonUpdateAssetList,
- downloadSpeedLimiter);
- }
- }
-
- // Get the remote chunk size
- _progressPerFileSizeTotal = sophonUpdateAssetList.GetCalculatedDiffSize(!isPreloadMode);
- _progressPerFileSizeCurrent = 0;
-
- // Get the remote total size and current total size
- _progressAllCountTotal = sophonUpdateAssetList.Count(x => !x.IsDirectory);
- _progressAllSizeTotal = !isPreloadMode
- ? sophonUpdateAssetList.Sum(x => x.AssetSize)
- : _progressPerFileSizeTotal;
- _progressAllSizeCurrent = 0;
-
- // Get the parallel options
- var parallelOptions = new ParallelOptions
- {
- MaxDegreeOfParallelism = maxThread,
- CancellationToken = _token.Token
- };
- var parallelChunksOptions = new ParallelOptions
- {
- MaxDegreeOfParallelism = maxChunksThread,
- CancellationToken = _token.Token
- };
-
- // Set the progress bar to indetermined
- _isSophonInUpdateMode = !isPreloadMode;
- _status.IsIncludePerFileIndicator = !isPreloadMode;
- _status.IsProgressPerFileIndetermined = false;
- _status.IsProgressAllIndetermined = false;
- _status.ActivityStatus = $"{(_isSophonInUpdateMode && !isPreloadMode
- ? Lang._Misc.UpdatingAndApplying
- : Lang._Misc.Downloading)}: {string.Format(Lang._Misc.PerFromTo, _progressAllCountCurrent,
- _progressAllCountTotal)}";
- UpdateStatus();
-
- // Get the update source and destination, also where the staging chunk files will be stored
- string chunkPath = _gameSophonChunkDir;
- string gamePath = _gamePath;
-
- // If the chunk directory is not exist, then create one.
- if (!Directory.Exists(chunkPath) && chunkPath != null)
- {
- Directory.CreateDirectory(chunkPath);
- }
-
- bool canDeleteChunks = _canDeleteZip;
- bool isSophonPreloadCompleted = _isSophonPreloadCompleted;
-
- // If preload completed and perf mode is on, use all CPU cores
- if (isSophonPreloadCompleted && LauncherConfig.GetAppConfigValue("SophonPreloadApplyPerfMode").ToBool())
- {
- maxThread = Environment.ProcessorCount;
- maxChunksThread = Math.Clamp(maxThread / 2, 2, 32);
-
- parallelOptions = new ParallelOptions
- {
- MaxDegreeOfParallelism = maxThread,
- CancellationToken = _token.Token
- };
- parallelChunksOptions = new ParallelOptions
- {
- MaxDegreeOfParallelism = maxChunksThread,
- CancellationToken = _token.Token
- };
- }
-
- var processingAsset = new ConcurrentDictionary();
- // Set the delegate function for the download action
- async ValueTask Action(SophonAsset asset, CancellationToken ctx)
- {
- if (!processingAsset.TryAdd(asset, 0))
- {
- LogWriteLine($"Found duplicate operation for {asset.AssetName}! Skipping...",
- LogType.Warning, true);
- return;
- }
- if (isPreloadMode)
- {
- // If preload mode, then only download the chunks
- await asset.DownloadDiffChunksAsync(httpClient, chunkPath, parallelChunksOptions,
- UpdateSophonFileTotalProgress, null,
- UpdateSophonDownloadStatus, isSophonPreloadCompleted);
- return;
- }
-
- // Ensure to remove the read-only attribute
- string currentAssetPath = Path.Combine(gamePath, asset.AssetName);
- TryUnassignReadOnlyFileSingle(currentAssetPath);
-
- // Otherwise, start the patching process
- await asset.WriteUpdateAsync(httpClient, gamePath, gamePath, chunkPath, canDeleteChunks,
- parallelChunksOptions, UpdateSophonFileTotalProgress,
- UpdateSophonFileDownloadProgress, UpdateSophonDownloadStatus);
- processingAsset.Remove(asset, out _);
- }
-
- // Enumerate in parallel and process the assets
- await Parallel.ForEachAsync(sophonUpdateAssetList.Where(x => !x.IsDirectory),
- parallelOptions,
- Action);
-
- _isSophonPreloadCompleted = isPreloadMode;
-
- // If it's in update mode, then clean up the temp sophon verified files
- if (!isPreloadMode)
- {
- CleanupTempSophonVerifiedFiles();
- }
-
- _isSophonDownloadCompleted = true;
- }
- finally
- {
- // Unsubscribe the logger event
- SophonLogger.LogHandler -= UpdateSophonLogHandler;
- httpClient.Dispose();
-
- // Check if the DXSETUP file is exist, then delete it.
- // The DXSETUP files causes some false positive detection of data modification
- // for some games (like Genshin, which causes 4302-x errors for some reason)
- string dxSetupDir = Path.Combine(_gamePath, "DXSETUP");
- TryDeleteReadOnlyDir(dxSetupDir);
- }
- }
-
- protected virtual void CleanupTempSophonVerifiedFiles()
- {
- DirectoryInfo dirPath = new (_gameSophonChunkDir);
- try
- {
- if (!dirPath.Exists)
- return;
-
- foreach (FileInfo file in dirPath.EnumerateFiles("*.verified", SearchOption.TopDirectoryOnly)
- .EnumerateNoReadOnly())
- {
- file.Delete();
- }
- }
- catch
- {
- // ignored
- }
- }
-
- private async Task AddSophonDiffAssetsToList(HttpClient httpClient,
- string requestedUrlFrom,
- string requestedUrlTo,
- List sophonPreloadAssetList,
- string matchingField,
- SophonDownloadSpeedLimiter downloadSpeedLimiter)
- {
- // Get the manifest pair for both previous (from) and next (to) version
- SophonChunkManifestInfoPair requestPairFrom = await SophonManifest
- .CreateSophonChunkManifestInfoPair(httpClient, requestedUrlFrom, matchingField, _token.Token);
- SophonChunkManifestInfoPair requestPairTo = await SophonManifest
- .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
- .EnumerateUpdateAsync(httpClient, requestPairFrom, requestPairTo,
- false, downloadSpeedLimiter)
- .WithCancellation(_token.Token))
- {
- sophonPreloadAssetList.Add(sophonAsset);
- }
- }
-
- private async Task AddSophonAdditionalVODiffAssetsToList(HttpClient httpClient,
- string requestedUrlFrom,
- string requestedUrlTo,
- List sophonPreloadAssetList,
- SophonDownloadSpeedLimiter downloadSpeedLimiter)
- {
- // Get the main VO language name from Id
- string mainLangId = GetLanguageLocaleCodeByID(_gameVoiceLanguageID);
- // Get the manifest pair for both previous (from) and next (to) version for the main VO
- await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo,
- sophonPreloadAssetList, mainLangId, downloadSpeedLimiter);
-
- // Check if the audio lang list file is exist, then try add others
- FileInfo fileInfo = new FileInfo(_gameAudioLangListPath).EnsureNoReadOnly();
- if (fileInfo.Exists)
- {
- // Use stream reader to read the list one-by-one
- using StreamReader reader = new StreamReader(_gameAudioLangListPath);
- // Read until EOF
- while (!reader.EndOfStream)
- {
- // Read line and if the line is equal, then skip
- string line = await reader.ReadLineAsync();
- if (line != null && line.Equals(mainLangId, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // Get other lang Id, pass it and try add to the list
- string otherLangId = GetLanguageLocaleCodeByLanguageString(line);
- // Get the manifest pair for both previous (from) and next (to) version for other VOs
- await AddSophonDiffAssetsToList(httpClient, requestedUrlFrom, requestedUrlTo,
- sophonPreloadAssetList, otherLangId, downloadSpeedLimiter);
- }
- }
- }
-
- private async ValueTask RunSophonAssetDownloadThread(HttpClient client, SophonAsset asset,
- ParallelOptions parallelOptions)
- {
- // 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 = EnsureCreationOfDirectory(Path.Combine(_gamePath, assetName));
-
- // Get the target and temp file info
- FileInfo existingFileInfo = new FileInfo(filePath);
- FileInfo sophonFileInfo = new FileInfo(filePath + "_tempSophon");
-
- // Remove read-only attribute
- if (existingFileInfo.Exists && existingFileInfo.IsReadOnly)
- {
- existingFileInfo.IsReadOnly = false;
- }
-
- if (sophonFileInfo.Exists && sophonFileInfo.IsReadOnly)
- {
- sophonFileInfo.IsReadOnly = false;
- }
-
- // Use "_tempSophon" if file is new or if "_tempSophon" file exist. Otherwise use original file if exist
- if (!existingFileInfo.Exists || sophonFileInfo.Exists
- || (existingFileInfo.Exists && sophonFileInfo.Exists))
- {
- filePath = sophonFileInfo.FullName;
- }
-
- // However if the file has already been existed and completely downloaded while _tempSophon is exist,
- // delete the _tempSophon one to avoid uncompleted files being applied instead.
- else if (existingFileInfo.Exists && existingFileInfo.Length == asset.AssetSize && sophonFileInfo.Exists)
- {
- sophonFileInfo.Delete();
- }
-
- await asset.WriteToStreamAsync(
- client,
- () => new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite,
- FileShare.ReadWrite),
- parallelOptions,
- UpdateSophonFileTotalProgress,
- UpdateSophonFileDownloadProgress,
- UpdateSophonDownloadStatus
- );
- }
-
private async ValueTask RunPackageVerificationRoutine(GameInstallPackage asset, CancellationToken token)
{
// Reset per size counter
@@ -1903,7 +1212,6 @@ public virtual void ApplyGameConfig(bool forceUpdateToLatest = false)
}
}
-
public virtual async ValueTask IsPreloadCompleted(CancellationToken token)
{
// If the game uses sophon download method, then check directly for the status
@@ -2579,7 +1887,7 @@ protected virtual string GetLanguageStringByID(int id)
};
}
- protected virtual string GetLanguageLocaleCodeByLanguageString([NotNull] string? langString)
+ protected virtual string? GetLanguageLocaleCodeByLanguageString([NotNullIfNotNull(nameof(langString))] string? langString, bool throwIfInvalid = true)
{
return langString switch
{
@@ -2588,7 +1896,9 @@ protected virtual string GetLanguageLocaleCodeByLanguageString([NotNull] string?
"English(US)" => "en-us",
"Korean" => "ko-kr",
"Japanese" => "ja-jp",
- _ => throw new NotSupportedException($"This language string: {langString} is not supported")
+ _ => throwIfInvalid
+ ? throw new NotSupportedException($"This language string: {langString} is not supported")
+ : null
};
}
@@ -2606,17 +1916,6 @@ protected virtual string GetLanguageLocaleCodeByLanguageString([NotNull] string?
};
}
- protected virtual int GetSophonLocaleCodeIndex(SophonData sophonData, string lookupName)
- {
- List localeList = sophonData.ManifestIdentityList
- .Where(x => IsValidLocaleCode(x.MatchingField))
- .Select(x => x.MatchingField.ToLower())
- .ToList();
-
- int index = localeList.IndexOf(lookupName);
- return Math.Max(0, index);
- }
-
protected virtual Dictionary GetLanguageDisplayDictFromVoicePackList(
List voicePacks)
{
@@ -2639,40 +1938,12 @@ protected virtual Dictionary GetLanguageDisplayDictFromVoicePack
return returnDict;
}
- protected virtual List GetSophonLanguageDisplayDictFromVoicePackList(SophonData sophonData)
- {
- var value = new List();
- foreach (SophonManifestIdentity identity in sophonData.ManifestIdentityList)
- {
- // Check the lang ID and add the translation of the language to the list
- string localeCode = identity.MatchingField.ToLower();
- if (IsValidLocaleCode(localeCode))
- {
- string? languageDisplay = GetLanguageDisplayByLocaleCode(localeCode, false);
- if (string.IsNullOrEmpty(languageDisplay))
- {
- continue;
- }
-
- value.Add(languageDisplay);
- }
- }
-
- 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)
{
// If the asset list is null or empty, return
@@ -2715,7 +1986,7 @@ protected virtual void RearrangeDataListLocaleOrder(List? assetDataList, F
assetDataList.AddRange(manifestListMain);
}
- protected virtual bool TryGetVoiceOverResourceByLocaleCode(List verResList,
+ protected virtual bool TryGetVoiceOverResourceByLocaleCode(List? verResList,
string localeCode, [NotNullWhen(true)] out RegionResourceVersion? outRes)
{
outRes = null;
@@ -2828,49 +2099,6 @@ protected virtual void WriteAudioLangList(List gamePackage)
sw.WriteLine(langString);
}
}
-
- protected virtual void WriteAudioLangListSophon(List sophonVOList)
- {
- // Create persistent directory if not exist
- if (!Directory.Exists(_gameDataPersistentPath))
- {
- Directory.CreateDirectory(_gameDataPersistentPath);
- }
-
- // If the game does not have audio lang list, then return
- if (string.IsNullOrEmpty(_gameAudioLangListPathStatic))
- {
- return;
- }
-
- // Read all the existing list
- List langList = File.Exists(_gameAudioLangListPathStatic)
- ? File.ReadAllLines(_gameAudioLangListPathStatic).ToList()
- : [];
-
- // Try lookup if there is a new language list, then add it to the list
- for (int index = 0; index < sophonVOList.Count; index++)
- {
- var packageLocaleCodeString = sophonVOList[index];
- string langString = GetLanguageStringByLocaleCode(packageLocaleCodeString);
- if (!langList.Contains(langString, StringComparer.OrdinalIgnoreCase))
- {
- langList.Add(langString);
- }
- }
-
- // Create the audio lang list file
- using var sw = new StreamWriter(_gameAudioLangListPathStatic,
- new FileStreamOptions
- { Mode = FileMode.Create, Access = FileAccess.Write });
- // Iterate the package list
- foreach (var voIds in langList)
- // Write the language string as per ID
- {
- sw.WriteLine(voIds);
- }
- }
-
#endregion
#region Private Methods - GetInstallationPath
@@ -3597,11 +2825,22 @@ protected virtual async ValueTask TryAddOtherInstalledVoicePacks(
// Start read the file
using StreamReader sw = new StreamReader(_gameAudioLangListPath);
- while (!sw.EndOfStream)
+#nullable enable
+ string? langStr;
+ while ((langStr = await sw.ReadLineAsync()) != null)
{
// Get the line and get the language locale code by language string
- string langStr = await sw.ReadLineAsync();
- string localeCode = GetLanguageLocaleCodeByLanguageString(langStr);
+ string? localeCode = GetLanguageLocaleCodeByLanguageString(langStr
+#if !DEBUG
+ , false
+#endif
+ );
+
+ if (string.IsNullOrEmpty(localeCode))
+ {
+ continue;
+ }
+#nullable restore
// Try get the voice over resource
if (TryGetVoiceOverResourceByLocaleCode(packs, localeCode, out RegionResourceVersion outRes))
@@ -3633,7 +2872,6 @@ protected virtual async ValueTask TryAddOtherInstalledVoicePacks(
#endregion
#region Private Methods - StartPackageInstallation
-
private void MoveFileToIngredientList(List assetIndex, string sourcePath,
string targetPath, bool isSR = false)
{
@@ -4020,15 +3258,15 @@ private async Task GetPackagesRemoteSize(List packageList, C
await Parallel.ForEachAsync(packageList, new ParallelOptions
{
CancellationToken = token
- }, async (package, _) =>
+ }, async (package, innerToken) =>
{
if (package.Segments != null)
{
- await TryGetSegmentedPackageRemoteSize(package, token);
+ await TryGetSegmentedPackageRemoteSize(package, innerToken);
return;
}
- await TryGetPackageRemoteSize(package, token);
+ await TryGetPackageRemoteSize(package, innerToken);
});
}
diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs
index 50c08cc10..ce9ff49ce 100644
--- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs
+++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs
@@ -466,7 +466,7 @@ protected void UpdateSophonDownloadStatus(SophonAsset asset)
UpdateStatus();
}
- protected void UpdateSophonLogHandler(object sender, LogStruct e)
+ protected void UpdateSophonLogHandler(object? sender, LogStruct e)
{
#if !DEBUG
if (e.LogLevel == LogLevel.Debug) return;
diff --git a/CollapseLauncher/Classes/RegionManagement/BridgedNetworkStream.cs b/CollapseLauncher/Classes/RegionManagement/BridgedNetworkStream.cs
index d04ded8e3..55b75b11b 100644
--- a/CollapseLauncher/Classes/RegionManagement/BridgedNetworkStream.cs
+++ b/CollapseLauncher/Classes/RegionManagement/BridgedNetworkStream.cs
@@ -14,7 +14,7 @@ partial class BridgedNetworkStream(HttpResponseMessage networkResponse, Stream n
internal static async ValueTask CreateStream(HttpResponseMessage networkResponse, CancellationToken token)
{
- Stream networkStream = await networkResponse?.Content.ReadAsStreamAsync(token)!;
+ Stream networkStream = await networkResponse.Content.ReadAsStreamAsync(token);
return new BridgedNetworkStream(networkResponse, networkStream);
}
diff --git a/CollapseLauncher/Classes/RegionManagement/RegionClasses.cs b/CollapseLauncher/Classes/RegionManagement/RegionClasses.cs
index cef25f5d0..ece0289f5 100644
--- a/CollapseLauncher/Classes/RegionManagement/RegionClasses.cs
+++ b/CollapseLauncher/Classes/RegionManagement/RegionClasses.cs
@@ -1,10 +1,6 @@
-using CollapseLauncher.Helper.Image;
-using CollapseLauncher.Helper.JsonConverter;
-using System;
+using CollapseLauncher.Helper.JsonConverter;
using System.Collections.Generic;
-using System.Linq;
using System.Text.Json.Serialization;
-using System.Threading;
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global
@@ -18,14 +14,6 @@ public interface IRegionResourceCopyable
T Copy();
}
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public enum PostCarouselType
- {
- POST_TYPE_INFO,
- POST_TYPE_ACTIVITY,
- POST_TYPE_ANNOUNCE
- }
-
public static class RegionResourceListHelper
{
public static List? Copy(this List? source)
@@ -36,7 +24,7 @@ public static class RegionResourceListHelper
return null;
}
- return source.Count == 0 ? new List() : new List(source);
+ return source.Count == 0 ? new List() : source;
}
}
@@ -60,11 +48,7 @@ public class RegionResourceGame : IRegionResourceCopyable
public List? plugins { get; set; }
public RegionResourceLatest? game { get; set; }
public RegionResourceLatest? pre_download_game { get; set; }
- public RegionBackgroundProp? adv { get; set; }
public RegionResourceVersion? sdk { get; set; }
- public List? banner { get; set; }
- public List? icon { get; set; }
- public List? post { get; set; }
public RegionResourceGame Copy()
{
@@ -73,11 +57,7 @@ public RegionResourceGame Copy()
plugins = plugins?.Copy(),
game = game?.Copy(),
pre_download_game = pre_download_game?.Copy(),
- adv = adv?.Copy(),
- sdk = sdk?.Copy(),
- banner = banner?.Copy(),
- icon = icon?.Copy(),
- post = post?.Copy()
+ sdk = sdk?.Copy()
};
}
}
@@ -184,247 +164,4 @@ public RegionResourceVersion Copy()
};
}
}
-
- public class HomeMenuPanel : IRegionResourceCopyable
- {
- public List? sideMenuPanel { get; set; }
- public List? imageCarouselPanel { get; set; }
- public PostCarouselTypes? articlePanel { get; set; }
- public RegionBackgroundProp? eventPanel { get; set; }
-
- public HomeMenuPanel Copy()
- {
- return new HomeMenuPanel()
- {
- sideMenuPanel = sideMenuPanel?.Copy(),
- imageCarouselPanel = imageCarouselPanel?.Copy(),
- articlePanel = articlePanel?.Copy(),
- eventPanel = eventPanel?.Copy()
- };
- }
- }
-
- public class PostCarouselTypes : IRegionResourceCopyable
- {
- public List? Events { get; set; } = new();
- public List? Notices { get; set; } = new();
- public List? Info { get; set; } = new();
-
- public PostCarouselTypes Copy()
- {
- return new PostCarouselTypes()
- {
- Events = Events?.Copy(),
- Notices = Notices?.Copy(),
- Info = Info?.Copy()
- };
- }
- }
-
- public class LinkProp : IRegionResourceCopyable
- {
- public string? title { get; set; }
- public string? url { get; set; }
-
- public LinkProp Copy()
- {
- return new LinkProp()
- {
- title = title,
- url = url
- };
- }
- }
-
- public struct MenuPanelProp(CancellationToken token = default) : IRegionResourceCopyable
- {
- private string? _icon;
- private string? _iconHover;
- private string? _qr;
-
- public string? URL { get; set; }
-
- public string? Icon
- {
- get => ImageLoaderHelper.GetCachedSprites(_icon, token);
- set => _icon = value;
- }
-
- public string? IconHover
- {
- get => ImageLoaderHelper.GetCachedSprites(_iconHover, token);
- set => _iconHover = value;
- }
-
- public string? QR
- {
- get => ImageLoaderHelper.GetCachedSprites(_qr, token);
- set => _qr = value;
- }
-
- public string? QR_Description { get; set; }
- public bool IsQRExist => !string.IsNullOrEmpty(QR);
- public string? Description { get; set; }
- public bool IsDescriptionExist => !string.IsNullOrEmpty(Description);
- public bool IsQRDescriptionExist => !string.IsNullOrEmpty(QR_Description);
- public List? Links { get; set; }
- public bool IsLinksExist => Links?.Any() == true;
- public bool ShowLinks => IsLinksExist && Links?.Count > 1;
- public bool ShowDescription => IsDescriptionExist && !ShowLinks;
-
- public MenuPanelProp Copy()
- {
- return new MenuPanelProp(token)
- {
- URL = URL,
- Icon = _icon,
- IconHover = _iconHover,
- QR = _qr,
- QR_Description = QR_Description,
- Description = Description,
- Links = Links?.Copy()
- };
- }
- }
-
- public class RegionBackgroundProp : IRegionResourceCopyable
- {
- public string? background { get; set; }
- public string? bg_checksum { get; set; }
- public string? icon { get; set; }
- public string? url { get; set; }
-
- [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
- public int? version { get; set; }
-
- public RegionBackgroundProp Copy()
- {
- return new RegionBackgroundProp()
- {
- background = background,
- bg_checksum = bg_checksum,
- icon = icon,
- url = url,
- version = version
- };
- }
- }
-
- public struct RegionSocMedProp : IRegionResourceCopyable
- {
- private string _url;
- private string _icon_link;
- private string _tittle;
- private List? _links;
- private List? _other_links;
-
- public string icon_id { get; set; }
-
- public string icon_link
- {
- get => StripTabsAndNewlines(string.IsNullOrEmpty(_icon_link) ? url : _icon_link);
- set => _icon_link = value;
- }
-
- public string img { get; set; }
- public string img_hover { get; set; }
- public string qr_img { get; set; }
- public string qr_desc { get; set; }
-
- public string url
- {
- get => _url;
- set => _url = StripTabsAndNewlines(value);
- }
-
- public string name { get; set; }
- public string title { get; set; }
-
- public string tittle
- {
- get => string.IsNullOrEmpty(_tittle) ? title : _tittle;
- set => _tittle = value;
- }
-
- public string show_time { get; set; }
- public PostCarouselType type { get; set; }
-
- public List? links
- {
- get => _links;
- set
- {
- _links = value;
- if (_links == null)
- {
- return;
- }
-
- foreach (var link in _links)
- {
- link.url = StripTabsAndNewlines(link.url);
- }
- }
- }
-
- public List? other_links
- {
- get => _other_links;
- set
- {
- _other_links = value;
- if (_other_links == null)
- {
- return;
- }
-
- foreach (var link in _other_links)
- {
- link.url = StripTabsAndNewlines(link.url);
- }
- }
- }
-
- private unsafe string StripTabsAndNewlines(ReadOnlySpan s)
- {
- int len = s.Length;
- char* newChars = stackalloc char[len];
- char* currentChar = newChars;
-
- for (int i = 0; i < len; ++i)
- {
- char c = s[i];
-
- if (c == '\r' || c == '\n' || c == '\t')
- {
- continue;
- }
-
- *currentChar++ = c;
- }
-
- return new string(newChars, 0, (int)(currentChar - newChars));
- }
-
- public RegionSocMedProp Copy()
- {
- return new RegionSocMedProp()
- {
- icon_id = icon_id,
- icon_link = icon_link,
- img = img,
- img_hover = img_hover,
- qr_img = qr_img,
- qr_desc = qr_desc,
- url = url,
- name = name,
- title = title,
- tittle = tittle,
- show_time = show_time,
- type = type,
- links = links?.Copy(),
- other_links = other_links?.Copy()
- };
- }
- }
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs b/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
index ef925afb1..e6b67f9f0 100644
--- a/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
+++ b/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
@@ -171,7 +171,7 @@ private async Task DownloadBackgroundImage(CancellationToken Token)
new FileInfo(LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.GameBackgroundImgLocal);
// Start downloading the background image
- var isDownloaded = await ImageLoaderHelper.DownloadAndEnsureCompleteness(imgFileInfo, true);
+ var isDownloaded = await ImageLoaderHelper.IsFileCompletelyDownloadedAsync(imgFileInfo, true);
if (isDownloaded)
{
@@ -202,10 +202,10 @@ private async Task DownloadBackgroundImage(CancellationToken Token)
var currentGameType = GamePropertyVault.GetCurrentGameProperty()._GameVersion.GameType;
tempImage ??= currentGameType switch
{
- GameNameType.Honkai => Path.Combine(AppFolder, @"Assets\Images\GamePoster\poster_honkai.png"),
- GameNameType.Genshin => Path.Combine(AppFolder, @"Assets\Images\GamePoster\poster_genshin.png"),
- GameNameType.StarRail => Path.Combine(AppFolder, @"Assets\Images\GamePoster\poster_starrail.png"),
- GameNameType.Zenless => Path.Combine(AppFolder, @"Assets\Images\GamePoster\poster_zzz.png"),
+ GameNameType.Honkai => Path.Combine(AppFolder, @"Assets\Images\GameBackground\honkai.webp"),
+ GameNameType.Genshin => Path.Combine(AppFolder, @"Assets\Images\GameBackground\genshin.webp"),
+ GameNameType.StarRail => Path.Combine(AppFolder, @"Assets\Images\GameBackground\starrail.webp"),
+ GameNameType.Zenless => Path.Combine(AppFolder, @"Assets\Images\GameBackground\zzz.webp"),
_ => AppDefaultBG
};
BackgroundImgChanger.ChangeBackground(tempImage, () =>
@@ -213,16 +213,18 @@ private async Task DownloadBackgroundImage(CancellationToken Token)
IsFirstStartup = false;
ColorPaletteUtility.ReloadPageTheme(this, CurrentAppTheme);
}, false, false, true);
- await ImageLoaderHelper.TryDownloadToCompleteness(
- LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.GameBackgroundImg,
- imgFileInfo,
- Token);
- BackgroundImgChanger.ChangeBackground(imgFileInfo.FullName, () =>
- {
- IsFirstStartup = false;
- ColorPaletteUtility.ReloadPageTheme(this, CurrentAppTheme);
- }, false, true, true);
- SetAndSaveConfigValue(lastBgCfg, imgFileInfo.FullName);
+ if (await ImageLoaderHelper.TryDownloadToCompletenessAsync(LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.GameBackgroundImg,
+ LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.ApiResourceHttpClient,
+ imgFileInfo,
+ Token))
+ {
+ BackgroundImgChanger.ChangeBackground(imgFileInfo.FullName, () =>
+ {
+ IsFirstStartup = false;
+ ColorPaletteUtility.ReloadPageTheme(this, CurrentAppTheme);
+ }, false, true, true);
+ SetAndSaveConfigValue(lastBgCfg, imgFileInfo.FullName);
+ }
#nullable disable
}
diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
index 4a40afe4a..0bec93acf 100644
--- a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
@@ -545,8 +545,22 @@ internal async void ChangeBackgroundImageAsRegionAsync(bool ShowLoadingMsg = fal
bool isUseCustomPerRegionBg = currentGameProperty?._GameSettings?.SettingsCollapseMisc?.UseCustomRegionBG ?? false;
IsCustomBG = GetAppConfigValue("UseCustomBG").ToBool();
- bool isAPIBackgroundAvailable = !string.IsNullOrEmpty(LauncherMetadataHelper.CurrentMetadataConfig?.GameLauncherApi?.GameBackgroundImg);
-
+ bool isAPIBackgroundAvailable =
+ !string.IsNullOrEmpty(LauncherMetadataHelper.CurrentMetadataConfig?.GameLauncherApi?.GameBackgroundImg);
+
+ var posterBg = currentGameProperty?._GameVersion.GameType switch
+ {
+ GameNameType.Honkai => Path.Combine(AppFolder,
+ @"Assets\Images\GameBackground\honkai.webp"),
+ GameNameType.Genshin => Path.Combine(AppFolder,
+ @"Assets\Images\GameBackground\genshin.webp"),
+ GameNameType.StarRail => Path.Combine(AppFolder,
+ @"Assets\Images\GameBackground\starrail.webp"),
+ GameNameType.Zenless => Path.Combine(AppFolder,
+ @"Assets\Images\GameBackground\zzz.webp"),
+ _ => AppDefaultBG
+ };
+
// Check if Regional Custom BG is enabled and available
if (isUseCustomPerRegionBg)
{
@@ -585,16 +599,16 @@ internal async void ChangeBackgroundImageAsRegionAsync(bool ShowLoadingMsg = fal
LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.GameBackgroundImgLocal = AppDefaultBG;
}
}
- // IF ITS STILL NOT THERE, then use paimon cute deadge pic :)
+ // IF ITS STILL NOT THERE, then use fallback game poster, IF ITS STILL NOT THEREEEE!! use paimon cute deadge pic :)
else
{
- gameLauncherApi.GameBackgroundImgLocal = AppDefaultBG;
+ gameLauncherApi.GameBackgroundImgLocal = posterBg;
}
}
// Use default background if the API background is empty (in-case HoYo did something catchy)
if (!isAPIBackgroundAvailable && !IsCustomBG && LauncherMetadataHelper.CurrentMetadataConfig?.GameLauncherApi != null)
- LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.GameBackgroundImgLocal = AppDefaultBG;
+ LauncherMetadataHelper.CurrentMetadataConfig.GameLauncherApi.GameBackgroundImgLocal ??= posterBg;
// If the custom per region is enabled, then execute below
BackgroundImgChanger.ChangeBackground(gameLauncherApi.GameBackgroundImgLocal,
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/CachesPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/Pages/CachesPage.xaml.cs
index ccddd7a3e..09e1d7514 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/CachesPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/CachesPage.xaml.cs
@@ -180,22 +180,38 @@ private void RemoveEvent()
private void _cacheTool_StatusChanged(object sender, TotalPerFileStatus e)
{
- DispatcherQueue?.TryEnqueue(() =>
+ if (!DispatcherQueue.HasThreadAccess)
+ {
+ DispatcherQueue?.TryEnqueue(Update);
+ return;
+ }
+
+ Update();
+ return;
+ void Update()
{
CachesDataTableGrid.Visibility = e.IsAssetEntryPanelShow ? Visibility.Visible : Visibility.Collapsed;
- CachesStatus.Text = e.ActivityStatus;
+ CachesStatus.Text = e.ActivityStatus;
- CachesTotalStatus.Text = e.ActivityAll;
+ CachesTotalStatus.Text = e.ActivityAll;
CachesTotalProgressBar.IsIndeterminate = e.IsProgressAllIndetermined;
- });
+ }
}
private void _cacheTool_ProgressChanged(object sender, TotalPerFileProgress e)
{
- DispatcherQueue?.TryEnqueue(() =>
+ if (!DispatcherQueue.HasThreadAccess)
+ {
+ DispatcherQueue?.TryEnqueue(Update);
+ return;
+ }
+
+ Update();
+ return;
+ void Update()
{
CachesTotalProgressBar.Value = e.ProgressAllPercentage;
- });
+ }
}
private void ResetStatusAndButtonState()
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml.cs
index 71809eb6a..166b2726e 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/FileCleanupPage.xaml.cs
@@ -309,7 +309,7 @@ private async Task PerformRemoval(List? deletionSource, long tota
if (isToRecycleBin)
{
// Get the list of the file to be deleted and add it to the deletedItems List if it exists
- List toBeDeleted = await Task.Run(() => deletionSource
+ List toBeDeleted = await Task.Factory.StartNew(() => deletionSource
.Select(x =>
{
var localFileInfo = x.ToFileInfo().EnsureNoReadOnly(out bool isFileExist);
@@ -322,14 +322,14 @@ private async Task PerformRemoval(List? deletionSource, long tota
return localFileInfo.FullName;
})
.Where(x => !string.IsNullOrEmpty(x))
- .ToList()).ConfigureAwait(false);
+ .ToList());
try
{
// Execute the deletion process
- var recycleBinTask = Task.Run(() => RecycleBin.MoveFileToRecycleBin(toBeDeleted, true));
- await recycleBinTask.ConfigureAwait(false);
+ var recycleBinTask = Task.Factory.StartNew(() => RecycleBin.MoveFileToRecycleBin(toBeDeleted, true));
+ await recycleBinTask;
deleteSuccess = toBeDeleted.Count;
}
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml
index 0ff8cdfc5..7c22eb7ff 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml
@@ -724,7 +724,8 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{x:Bind VolumetricFog, Mode=TwoWay}">
-
@@ -735,7 +736,8 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{x:Bind Reflections, Mode=TwoWay}">
-
@@ -745,7 +747,8 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{x:Bind Bloom, Mode=TwoWay}">
-
@@ -764,7 +767,8 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{x:Bind TeamPageBackground, Mode=TwoWay}">
-
@@ -775,7 +779,8 @@
VerticalAlignment="Center"
IsChecked="{x:Bind DynamicCharacterResolution, Mode=TwoWay}"
ToolTipService.ToolTip="{x:Bind helper:Locale.Lang._GenshinGameSettingsPage.Graphics_DynamicCharacterResolution_Tooltip}">
-
@@ -863,38 +868,39 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml
index 3d527b7e1..766fc769a 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml
@@ -437,7 +437,10 @@
-
+
@@ -644,16 +647,15 @@
Height="{x:Bind localWindowSize:WindowSize.CurrentWindowSize.PostPanelBounds.Height}"
Padding="8,6,8,0"
Visibility="{x:Bind IsPostInfoPanelAllEmpty}">
-
-
+
-
-
+
-
+
-
-
+
-
+
-
-
+
+
+
+
+
+
+
@@ -1883,6 +1911,10 @@
Text="{x:Bind CustomArgsValue, Mode=TwoWay}"
TextWrapping="Wrap" />
+
+
-
- 0; i--)
{
- ImageCarousel.SelectedIndex = i - 1;
+ if (i - 1 >= 0 && i - 1 < ImageCarousel.Items.Count)
+ {
+ ImageCarousel.SelectedIndex = i - 1;
+ }
if (CarouselToken is { IsDisposed: false, IsCancellationRequested: false })
{
await Task.Delay(100, CarouselToken.Token);
@@ -441,8 +447,8 @@ public async Task StartCarouselAutoScroll(int delaySeconds = 5)
}
catch (Exception ex)
{
- await SentryHelper.ExceptionHandlerAsync(ex, SentryHelper.ExceptionType.UnhandledOther);
LogWriteLine($"[HomePage::StartCarouselAutoScroll] Task returns error!\r\n{ex}", LogType.Error, true);
+ _ = CarouselRestartScroll();
}
}
@@ -600,6 +606,10 @@ private void ShowSocMedFlyout(object sender, RoutedEventArgs e)
ToolTip tooltip = sender as ToolTip;
if (tooltip?.Tag is Button button)
{
+ if (!button.IsPointerOver && lastSocMedButton == button)
+ return;
+ lastSocMedButton = button;
+
Flyout flyout = button.Flyout as Flyout;
if (flyout != null)
{
@@ -612,10 +622,9 @@ private void ShowSocMedFlyout(object sender, RoutedEventArgs e)
}
}
}
- }
-
- FlyoutBase.ShowAttachedFlyout(tooltip?.Tag as FrameworkElement);
+ FlyoutBase.ShowAttachedFlyout(button);
+ }
}
private void HideSocMedFlyout(object sender, RoutedEventArgs e)
@@ -1074,14 +1083,15 @@ private async void CheckRunningGameInstance(CancellationToken Token)
// HACK: For some reason, the text still unchanged.
// Make sure the start game button text also changed.
StartGameBtnText.Text = Lang._HomePage.StartBtnRunning;
- DateTime fromActivityOffset = currentGameProcess.StartTime;
+ var fromActivityOffset = currentGameProcess.StartTime;
+ var gameSettings = CurrentGameProperty!._GameSettings!.AsIGameSettingsUniversal();
+ var gamePreset = CurrentGameProperty._GamePreset;
+
#if !DISABLEDISCORD
- AppDiscordPresence?.SetActivity(ActivityType.Play, fromActivityOffset.ToUniversalTime());
+ if (ToggleRegionPlayingRpc)
+ AppDiscordPresence?.SetActivity(ActivityType.Play, fromActivityOffset.ToUniversalTime());
#endif
- IGameSettingsUniversal gameSettings = CurrentGameProperty!._GameSettings!.AsIGameSettingsUniversal();
- PresetConfig gamePreset = CurrentGameProperty._GamePreset;
-
CurrentGameProperty!._GamePlaytime!.StartSession(currentGameProcess);
int? height = gameSettings.SettingsScreen.height;
@@ -3250,5 +3260,19 @@ await PreloadDialogBox.StartAnimation(TimeSpan.FromSeconds(0.5),
compositor.CreateVector3KeyFrameAnimation("Translation", PreloadDialogBox.Translation, toTranslate)
);
}
+
+ private bool? _regionPlayingRpc;
+ private bool ToggleRegionPlayingRpc
+ {
+ get => _regionPlayingRpc ??= CurrentGameProperty._GameSettings.AsIGameSettingsUniversal()
+ .SettingsCollapseMisc.IsPlayingRpc;
+ set
+ {
+ CurrentGameProperty._GameSettings.AsIGameSettingsUniversal()
+ .SettingsCollapseMisc.IsPlayingRpc = value;
+ _regionPlayingRpc = value;
+ CurrentGameProperty._GameSettings.SaveSettings();
+ }
+ }
}
}
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/RepairPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/Pages/RepairPage.xaml.cs
index 8883da5d0..ecb91c2bc 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/RepairPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/RepairPage.xaml.cs
@@ -15,12 +15,13 @@
using static Hi3Helper.Locale;
using static Hi3Helper.Logger;
using static Hi3Helper.Shared.Region.LauncherConfig;
+// ReSharper disable RedundantExtendsListEntry
namespace CollapseLauncher.Pages
{
public sealed partial class RepairPage : Page
{
- private GamePresetProperty CurrentGameProperty { get; set; }
+ private GamePresetProperty CurrentGameProperty { get; }
public RepairPage()
{
BackgroundImgChanger.ToggleBackground(true);
@@ -103,12 +104,12 @@ private async void RunCheckRoutine(object sender, bool isFast, bool isMainButton
private async void StartGameRepair(object sender, RoutedEventArgs e)
{
- Sleep.PreventSleep(ILoggerHelper.GetILogger());
- RepairFilesBtn.IsEnabled = false;
- CancelBtn.IsEnabled = true;
-
try
{
+ Sleep.PreventSleep(ILoggerHelper.GetILogger());
+ RepairFilesBtn.IsEnabled = false;
+ CancelBtn.IsEnabled = true;
+
AddEvent();
int assetCount = CurrentGameProperty._GameRepair.AssetEntry.Count;
@@ -182,7 +183,15 @@ private void RemoveEvent()
private void _repairTool_StatusChanged(object sender, TotalPerFileStatus e)
{
- DispatcherQueue?.TryEnqueue(() =>
+ if (!DispatcherQueue.HasThreadAccess)
+ {
+ DispatcherQueue?.TryEnqueue(Update);
+ return;
+ }
+
+ Update();
+ return;
+ void Update()
{
RepairDataTableGrid.Visibility = e.IsAssetEntryPanelShow ? Visibility.Visible : Visibility.Collapsed;
RepairStatus.Text = e.ActivityStatus;
@@ -191,16 +200,24 @@ private void _repairTool_StatusChanged(object sender, TotalPerFileStatus e)
RepairTotalStatus.Text = e.ActivityAll;
RepairTotalProgressBar.IsIndeterminate = e.IsProgressAllIndetermined;
RepairPerFileProgressBar.IsIndeterminate = e.IsProgressPerFileIndetermined;
- });
+ }
}
private void _repairTool_ProgressChanged(object sender, TotalPerFileProgress e)
{
- DispatcherQueue?.TryEnqueue(() =>
+ if (!DispatcherQueue.HasThreadAccess)
{
- RepairPerFileProgressBar.Value = e.ProgressPerFilePercentage;
- RepairTotalProgressBar.Value = e.ProgressAllPercentage;
- });
+ DispatcherQueue?.TryEnqueue(Update);
+ return;
+ }
+
+ Update();
+ return;
+ void Update()
+ {
+ RepairPerFileProgressBar.Value = Math.Min(e.ProgressPerFilePercentage, 100);
+ RepairTotalProgressBar.Value = Math.Min(e.ProgressAllPercentage, 100);
+ }
}
private void ResetStatusAndButtonState()
@@ -244,12 +261,12 @@ private void InitializeLoaded(object sender, RoutedEventArgs e)
OverlayTitle.Text = Lang._GameRepairPage.OverlayGameRunningTitle;
OverlaySubtitle.Text = Lang._GameRepairPage.OverlayGameRunningSubtitle;
}
+ #if !DISABLEDISCORD
else
{
-#if !DISABLEDISCORD
InnerLauncherConfig.AppDiscordPresence?.SetActivity(ActivityType.Repair);
-#endif
}
+ #endif
}
}
}
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml
index 657883abd..870590dc1 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml
@@ -1156,6 +1156,7 @@
HorizontalContentAlignment="Stretch"
Click="GenerateGuidButton_Click"
CornerRadius="14"
+ IsEnabled="{Binding Path=IsOn, ElementName=DbToggle}"
Style="{ThemeResource AccentButtonStyle}">
@@ -1181,6 +1182,7 @@
HorizontalContentAlignment="Stretch"
Click="ValidateAndSaveDbButton_Click"
CornerRadius="14"
+ IsEnabled="{Binding Path=IsOn, ElementName=DbToggle}"
Style="{ThemeResource AccentButtonStyle}">
diff --git a/Hi3Helper.Core/Classes/SentryHelper/SentryExceptionFilter.cs b/Hi3Helper.Core/Classes/SentryHelper/SentryExceptionFilter.cs
new file mode 100644
index 000000000..c71d42415
--- /dev/null
+++ b/Hi3Helper.Core/Classes/SentryHelper/SentryExceptionFilter.cs
@@ -0,0 +1,54 @@
+using Sentry.Extensibility;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Sockets;
+
+namespace Hi3Helper.SentryHelper
+{
+ public class NetworkException : IExceptionFilter
+ {
+ private static readonly HashSet SocketExceptionFilters =
+ [
+ 10050, // WSAENETDOWN - The network subsystem has failed.
+ 10051, // WSAENETUNREACH - The network can't be reached from this host at this time.
+ 10052, // WSAENETRESET - The connection has been broken due to keep-alive activity detecting a failure while the operation was in progress.
+ 10053, // WSAECONNABORTED - The connection was aborted due to a network error.
+ 10054, // WSAECONNRESET - The connection was reset by the remote peer.
+ 10060, // WSAETIMEDOUT - The connection has been dropped because of a network failure or because the peer system failed to respond.
+ 10061, // WSAECONNREFUSED - The connection was refused by the remote host.
+ 10065 // WSAEHOSTUNREACH - The host is unreachable.
+ ];
+
+ private static readonly HashSet HttpExceptionFilters =
+ [
+ "net_http_client_execution_error",
+ "net_http_request_aborted"
+ ];
+
+ private static readonly HashSet HttpRequestErrorFilters =
+ [
+ HttpRequestError.NameResolutionError,
+ HttpRequestError.SecureConnectionError
+ ];
+
+ public bool Filter(Exception ex)
+ {
+ var returnValue = ex switch
+ {
+ SocketException socketEx => SocketExceptionFilters.Contains(socketEx.ErrorCode),
+ HttpRequestException httpEx => HttpRequestErrorFilters.Contains(httpEx.HttpRequestError)
+ || HttpExceptionFilters.Any(f => httpEx.Message.Contains(f)),
+ _ => false
+ };
+
+ if (returnValue)
+ {
+ Logger.LogWriteLine("[Sentry] Filtered exception: " + ex.Message, LogType.Sentry);
+ }
+
+ return returnValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Hi3Helper.Core/Classes/SentryHelper/SentryHelper.cs b/Hi3Helper.Core/Classes/SentryHelper/SentryHelper.cs
index ef39deedf..b5b7fc2fc 100644
--- a/Hi3Helper.Core/Classes/SentryHelper/SentryHelper.cs
+++ b/Hi3Helper.Core/Classes/SentryHelper/SentryHelper.cs
@@ -6,6 +6,7 @@
using Microsoft.Win32;
using Sentry.Infrastructure;
using Sentry.Protocol;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -142,6 +143,7 @@ public static void InitializeSentrySdk()
o.MaxAttachmentSize = SentryMaxAttachmentSize;
o.DeduplicateMode = DeduplicateMode.All;
o.Environment = Debugger.IsAttached ? "debug" : IsPreview ? "non-debug" : "stable";
+ o.AddExceptionFilter(new NetworkException());
});
SentrySdk.ConfigureScope(s =>
{
@@ -251,8 +253,8 @@ public static async Task ExceptionHandlerAsync(Exception ex, ExceptionType exT =
return;
}
- ExceptionHandler(ex, exT);
-
+ await Task.Run(() => ExceptionHandler(ex, exT));
+ await Task.Delay(250); // Delay to ensure that the exception is uploaded
await Task.Run(async () => await SentrySdk.FlushAsync(TimeSpan.FromSeconds(10)));
}
@@ -314,7 +316,7 @@ public static async Task ExceptionHandler_ForLoopAsync(Exception ex, ExceptionTy
_loopToken = new CancellationTokenSource(); // Create new token
_exHLoopLastEx = ex;
ExHLoopLastEx_AutoClean(); // Start auto clean loop
- ExceptionHandlerInner(ex, exT);
+ await Task.Run(() => ExceptionHandlerInner(ex, exT));
}
#region Breadcrumbs Data
@@ -456,12 +458,33 @@ private static Breadcrumb GpuInfo
#endregion
- private static void ExceptionHandlerInner(Exception ex, ExceptionType exT = ExceptionType.Handled)
+ private static Task ExceptionHandlerInner(Exception ex, ExceptionType exT = ExceptionType.Handled)
{
SentrySdk.AddBreadcrumb(BuildInfo);
SentrySdk.AddBreadcrumb(GameInfo);
SentrySdk.AddBreadcrumb(CpuInfo);
SentrySdk.AddBreadcrumb(GpuInfo);
+
+ var loadedModules = Process.GetCurrentProcess().Modules;
+ var modulesInfo = new ConcurrentDictionary();
+
+ Parallel.ForEach(loadedModules.Cast(), module =>
+ {
+ try
+ {
+ var name = module.ModuleName;
+ var ver = module.FileVersionInfo.FileVersion;
+ var path = module.FileName;
+ _ = modulesInfo.TryAdd(name, $"{ver} ({path})");
+ }
+ catch (Exception exI)
+ {
+ Logger.LogWriteLine($"Failed to get module info: {exI.Message}", LogType.Error, true);
+ }
+ });
+
+ SentrySdk.AddBreadcrumb("Loaded Modules", "AppInfo",
+ "system.module", modulesInfo.ToDictionary());
ex.Data[Mechanism.HandledKey] ??= exT == ExceptionType.Handled;
@@ -479,7 +502,7 @@ private static void ExceptionHandlerInner(Exception ex, ExceptionType exT = Exce
ExceptionType.UnhandledOther => methodName ?? ex.Source ?? "Application.UnhandledException",
_ => methodName ?? ex.Source ?? "Application.HandledException"
};
-
+
#pragma warning disable CS0162 // Unreachable code detected
if (SentryUploadLog) // Upload log file if enabled
// ReSharper disable once HeuristicUnreachableCode
@@ -497,6 +520,7 @@ private static void ExceptionHandlerInner(Exception ex, ExceptionType exT = Exce
SentrySdk.CaptureException(ex);
}
#pragma warning restore CS0162 // Unreachable code detected
+ return Task.CompletedTask;
}
[GeneratedRegex(@"(?<=\bat\s)(CollapseLauncher|Hi3Helper)\.[^\s(]+", RegexOptions.Compiled)]
diff --git a/Hi3Helper.Core/Lang/Locale/LangHomePage.cs b/Hi3Helper.Core/Lang/Locale/LangHomePage.cs
index 97e6cdbe4..14245dd06 100644
--- a/Hi3Helper.Core/Lang/Locale/LangHomePage.cs
+++ b/Hi3Helper.Core/Lang/Locale/LangHomePage.cs
@@ -50,7 +50,9 @@ public sealed partial class LangHomePage
public string GameSettings_Panel2MoveGameLocationGame { get; set; } = LangFallback?._HomePage.GameSettings_Panel2MoveGameLocationGame;
public string GameSettings_Panel2MoveGameLocationGame_SamePath { get; set; } = LangFallback?._HomePage.GameSettings_Panel2MoveGameLocationGame_SamePath;
public string GameSettings_Panel2StopGame { get; set; } = LangFallback?._HomePage.GameSettings_Panel2StopGame;
+ public string GameSettings_Panel3RegionalSettings { get; set; } = LangFallback?._HomePage.GameSettings_Panel3RegionalSettings;
public string GameSettings_Panel3 { get; set; } = LangFallback?._HomePage.GameSettings_Panel3;
+ public string GameSettings_Panel3RegionRpc { get; set; } = LangFallback?._HomePage.GameSettings_Panel3RegionRpc;
public string GameSettings_Panel3CustomBGRegion { get; set; } = LangFallback?._HomePage.GameSettings_Panel3CustomBGRegion;
public string GameSettings_Panel3CustomBGRegionSectionTitle { get; set; } = LangFallback?._HomePage.GameSettings_Panel3CustomBGRegionSectionTitle;
public string GameSettings_Panel4 { get; set; } = LangFallback?._HomePage.GameSettings_Panel4;
diff --git a/Hi3Helper.Core/Lang/en_US.json b/Hi3Helper.Core/Lang/en_US.json
index 8243d9936..4b2a81646 100644
--- a/Hi3Helper.Core/Lang/en_US.json
+++ b/Hi3Helper.Core/Lang/en_US.json
@@ -150,10 +150,12 @@
"GameSettings_Panel2MoveGameLocationGame": "Move Game Location",
"GameSettings_Panel2MoveGameLocationGame_SamePath": "Cannot move game to the root of your drive!\r\nPlease make a folder and try again.",
"GameSettings_Panel2StopGame": "Force Close Game",
+ "GameSettings_Panel3RegionalSettings": "Regional Settings",
"GameSettings_Panel3": "Custom Start-up Args",
+ "GameSettings_Panel3RegionRpc": "Show Playing Status in Discord",
"GameSettings_Panel3CustomBGRegion": "Change Region Background",
"GameSettings_Panel3CustomBGRegionSectionTitle": "Custom Background for Region",
- "GameSettings_Panel4": "Miscellaneous",
+ "GameSettings_Panel4": "Global - Miscellaneous",
"GameSettings_Panel4ShowEventsPanel": "Show Events Panel",
"GameSettings_Panel4ShowSocialMediaPanel": "Show Social Media Panel",
"GameSettings_Panel4ShowPlaytimeButton": "Show Game Playtime",
diff --git a/Hi3Helper.Core/Lang/es_419.json b/Hi3Helper.Core/Lang/es_419.json
index 7e5b04127..69299ca93 100644
--- a/Hi3Helper.Core/Lang/es_419.json
+++ b/Hi3Helper.Core/Lang/es_419.json
@@ -150,10 +150,12 @@
"GameSettings_Panel2MoveGameLocationGame": "Mover Carpeta del Juego",
"GameSettings_Panel2MoveGameLocationGame_SamePath": "¡No se puede mover el juego a la carpeta raíz de tu disco!\r\nPor favor, crea una carpeta e inténtalo de nuevo.",
"GameSettings_Panel2StopGame": "Forzar el cierre del juego",
+ "GameSettings_Panel3RegionalSettings": "Ajustes regionales",
"GameSettings_Panel3": "Argumentos Personalizados de Arranque",
+ "GameSettings_Panel3RegionRpc": "Mostrar estado de juego en Discord",
"GameSettings_Panel3CustomBGRegion": "Cambiar Fondo de región",
"GameSettings_Panel3CustomBGRegionSectionTitle": "Fondo Personalizado por Región",
- "GameSettings_Panel4": "Otros",
+ "GameSettings_Panel4": "Global - Otros",
"GameSettings_Panel4ShowEventsPanel": "Mostrar Panel de Eventos",
"GameSettings_Panel4ShowSocialMediaPanel": "Mostrar Redes Sociales",
"GameSettings_Panel4ShowPlaytimeButton": "Mostrar tiempo de juego",
@@ -322,7 +324,7 @@
"Graphics_APIHelp2": "Aviso:",
"Graphics_APIHelp3": "Usar DirectX 12 puede causar fallos en algunos escenarios, ya que el juego no fue compilado nativamente con la API de DirectX 12",
- "Graphics_SpecPanel": "Configuración Global de Gráficos",
+ "Graphics_SpecPanel": "Ajustes Gráficos Globales",
"Graphics_Preset": "Ajustes Gráficos Predefinidos",
"Graphics_Render": "Calidad de Renderizado",
"Graphics_Shadow": "Calidad de Sombras",
@@ -335,7 +337,7 @@
"Graphics_FXAA": "FXAA",
"Graphics_FXDistort": "Distorción",
- "Graphics_APHO2Panel": "Configuración Gráfica para APHO2 y Nuevos Capítulos",
+ "Graphics_APHO2Panel": "Ajustes gráficos para APHO2 y capítulos nuevos",
"Graphics_APHO2GI": "Iluminación Global",
"Graphics_APHO2VL": "Iluminación Volumétrica",
"Graphics_APHO2AO": "Oclusión Ambiental",
@@ -420,7 +422,7 @@
"_SettingsPage": {
"PageTitle": "Ajustes de la Aplicación",
- "Debug": "Configuraciones adicionales",
+ "Debug": "Ajustes adicionales",
"Debug_Console": "Mostrar Consola",
"Debug_IncludeGameLogs": "Guardar registros de juego en Collapse (podría contener datos sensibles)",
"Debug_SendRemoteCrashData": "Enviar anónimamente informes de errores a los desarrolladores",
@@ -883,7 +885,7 @@
"InstallingMediaPackTitle": "Instalando Paquete de Características Multimedia",
"InstallingMediaPackSubtitle": "Esperando a que que se complete la instalación...",
"InstallingMediaPackSubtitleFinished": "Finalizado",
- "GameConfigBrokenTitle1": "Configuración del Juego Roto",
+ "GameConfigBrokenTitle1": "Configuración corrupta del Juego",
"GameConfigBrokenSubtitle1": "La configuración del Launcher esta corrupta. Por favor selecciona la ubicación del juego manualmente.",
"GameConfigBrokenSubtitle2": "\r\nAsegúrate de que la ubicación del juego no esté en el mismo directorio en el que está el archivo `config.ini` de tu launcher, que se encuentra en:\r\n\r\n{0}\r\n\r\n",
"GameConfigBrokenSubtitle3": "Si lo anterior es válido, por favor mueve todos los archivos del juego a otra ubicación, luego haz clic en \"Ubicar Directorio\" para elegir la ruta.",
@@ -1149,6 +1151,10 @@
"ApplyUpdateErrCollapseRunTitle": "¡Por favor, cierra Collapse antes de actualizar! ",
"ApplyUpdateErrCollapseRunSubtitle": "Esperando que Collapse se cierre...",
+ "ApplyUpdateErrCollapseRunTitleWarnBox": "¡Una instancia de Collapse Launcher sigue funcionando!",
+ "ApplyUpdateErrCollapseRunSubtitleWarnBox": "Hemos detectado que una instancia del Launcher esta funcionando en el fondo. Para forzar el cierre, haz clic en \"Si\". Para esperar hasta que lo cierres manualmente, haz clic en \"No\".",
+ "ApplyUpdateErrVelopackStateBrokenTitleWarnBox": "¡Instalación Corrupta Detectada!",
+ "ApplyUpdateErrVelopackStateBrokenSubtitleWarnBox": "Hemos detectado que tienes una instalación corrupta.\r\n\r\nHaz clic en \"Si\" para comenzar la reparación antes de instalar la actualización o clic en \"No\" para solo instalar la actualización.",
"ApplyUpdateErrReleaseFileNotFoundTitle": "ERROR:\r\nEl archivo \"release\" no tiene el string \"stable\" o \"preview\" en él",
"ApplyUpdateErrReleaseFileNotFoundSubtitle": "Por favor verifica el archivo \"release\" e intenta de nuevo",
@@ -1201,7 +1207,7 @@
"_StarRailGameSettingsPage": {
"PageTitle": "Ajustes del Juego",
- "Graphics_Title": "Configuración Gráfica",
+ "Graphics_Title": "Ajustes Gráficos",
"Graphics_ResolutionPanel": "Resolución del Juego",
"Graphics_Fullscreen": "Pantalla Completa",
"Graphics_ExclusiveFullscreen": "Pantalla Completa Exclusiva",
@@ -1229,7 +1235,7 @@
"Graphics_SelfShadow": "Sombra de personaje en mundo abierto",
"Graphics_HalfResTransparent": "Resolución media en transparencia ",
- "Graphics_SpecPanel": "Configuración Grafica Global",
+ "Graphics_SpecPanel": "Ajustes Gráficos Globales",
"SpecEnabled": "Habilitado",
"SpecDisabled": "Desactivado",
"SpecVeryLow": "Muy Bajo",
@@ -1254,14 +1260,14 @@
"CustomArgs_Footer2": "Unity Standalone Command-line documentation (en Ingles)",
"CustomArgs_Footer3": "para ver mas parámetros.",
- "Audio_Title": "Configuración de Audio",
+ "Audio_Title": "Ajustes de Audio",
"Audio_Master": "Volumen Principal",
"Audio_BGM": "Volumen BGM",
"Audio_SFX": "Volumen Efectos de Sonido",
"Audio_VO": "Volumen Voces",
"Audio_Mute": "Silenciar Audio",
- "Language": "Configuración de Idioma",
+ "Language": "Ajustes de Idioma",
"Language_Help1": "Por el momento Collapse no puede descargar el paquete de audio.",
"Language_Help2": "El paquete de audio se descargará dentro del juego la próxima vez que se inicie.",
"LanguageAudio": "Voces",
@@ -1275,7 +1281,7 @@
"_GenshinGameSettingsPage": {
"PageTitle": "Ajustes del Juego",
- "Graphics_Title": "Configuración Gráfica",
+ "Graphics_Title": "Ajustes Gráficos",
"Graphics_ResolutionPanel": "Resolución del juego",
"Graphics_Fullscreen": "Pantalla Completa",
"Graphics_ExclusiveFullscreen": "Usar Pantalla Completa Exclusiva",
@@ -1324,7 +1330,7 @@
"Graphics_HDR_SceneBrightness": "Brillo del paisaje",
"Graphics_HDR_SceneBrightness_Help": "Controla qué tan brillante debería ser un paisaje.",
- "Graphics_SpecPanel": "Configuración Grafica Global",
+ "Graphics_SpecPanel": "Ajustes Gráficos Globales",
"SpecEnabled": "Activado",
"SpecDisabled": "Desactivado",
"SpecVeryLow": "Muy Bajo",
@@ -1351,7 +1357,7 @@
"CustomArgs_Footer2": "Unity Standalone Command-line documentation (en Ingles)",
"CustomArgs_Footer3": "para ver mas parámetros.",
- "Audio_Title": "Configuración de Audio",
+ "Audio_Title": "Ajustes de Audio",
"Audio_Master": "Volumen Principal",
"Audio_BGM": "Volumen BGM",
"Audio_SFX": "Volumen Efectos de Sonido",
@@ -1361,7 +1367,7 @@
"Audio_DynamicRange": "Rango Dinámico completo",
"Audio_MuteOnMinimize": "Silenciar Audio al Minimizar",
- "Language": "Configuración de Idioma",
+ "Language": "Ajustes de Idioma",
"Language_Help1": "Por el momento Collapse no puede descargar el paquete de audio.",
"Language_Help2": "El paquete de audio se descargará dentro del juego la próxima vez que se inicie.",
"LanguageAudio": "Voces",
diff --git a/Hi3Helper.Core/Lang/id_ID.json b/Hi3Helper.Core/Lang/id_ID.json
index 6c143bee4..e7d2e4a11 100644
--- a/Hi3Helper.Core/Lang/id_ID.json
+++ b/Hi3Helper.Core/Lang/id_ID.json
@@ -150,10 +150,12 @@
"GameSettings_Panel2MoveGameLocationGame": "Pindah Lokasi Game",
"GameSettings_Panel2MoveGameLocationGame_SamePath": "Kamu tidak bisa memilih lokasi root dari drivemu!\r\nMohon buat folder baru dan coba lagi.",
"GameSettings_Panel2StopGame": "Tutup Paksa Game",
+ "GameSettings_Panel3RegionalSettings": "Pengaturan Regional",
"GameSettings_Panel3": "Penyesuaian Argumen Pemulaian",
+ "GameSettings_Panel3RegionRpc": "Munculkan Status Bermain di Discord",
"GameSettings_Panel3CustomBGRegion": "Ubah Latar Belakang Regional",
"GameSettings_Panel3CustomBGRegionSectionTitle": "Latar Belakang Lainnya untuk Region",
- "GameSettings_Panel4": "Lainnya",
+ "GameSettings_Panel4": "Global - Lainnya",
"GameSettings_Panel4ShowEventsPanel": "Munculkan Panel Event",
"GameSettings_Panel4ShowSocialMediaPanel": "Munculkan Panel Sosmed",
"GameSettings_Panel4ShowPlaytimeButton": "Munculkan Tombol \"Waktu Main\"",
@@ -436,7 +438,7 @@
"AppLang_ApplyNeedRestart": "*Kamu perlu memulai ulang launcher untuk menerapkan bahasa ini.",
"AppThemes": "Tema",
- "AppThemes_Default": "Asal (Menggunakan pengaturan sistem)",
+ "AppThemes_Default": "Bawaan (Menggunakan pengaturan sistem)",
"AppThemes_Light": "Terang",
"AppThemes_Dark": "Gelap",
"AppThemes_ApplyNeedRestart": "*Kamu perlu memulai ulang launcher untuk menerapkan tema ini.",
@@ -458,7 +460,7 @@
"AppThreads_Download": "Utas Unduhan",
"AppThreads_Extract": "Utas Ekstrak",
"AppThreads_Help1": "Utas ini akan bertanggungjawab untuk menentukan berapa banyak potongan yang akan dibagi pada saat mengunduh file. Mekanisme ini mirip dengan apa yang IDM dan aria2c lakukan.",
- "AppThreads_Help2": "Nilai asal:",
+ "AppThreads_Help2": "Nilai bawaan:",
"AppThreads_Help3": "Rentang nilai:",
"AppThreads_Help4": "0 (Deteksi otomatis)",
"AppThreads_Help5": "Utas ini akan bertanggungjawab untuk menangani utas-utas untuk proses ekstrak/verifikasi pada saat memasang/memulihkan game.",
@@ -538,7 +540,7 @@
"Disclaimer1": "Aplikasi ini tidak berkaitan dengan",
"Disclaimer2": "dengan cara apapun",
"Disclaimer3": "dan sepenuhnya \"Open-Source\". Kontribusi apapun dipersilahkan.",
-
+ "WebsiteBtn": "Kunjungi Website Kami",
"DiscordBtn1": "Gabung Discord Armada Kami",
"DiscordBtn2": "Kunjungi Discord Honkai Impact 3rd",
"DiscordBtn3": "Discord Resmi Collapse!",
@@ -747,6 +749,7 @@
"Disabled": "Mati",
"Enabled": "Nyala",
"UseAsDefault": "Gunakan sebagai Bawaan",
+ "Default": "Bawaan",
"BuildChannelPreview": "Preview",
"BuildChannelStable": "Stable",
@@ -792,7 +795,7 @@
"IsBytesMoreThanBytes": "= {0} bytes (-/+ {1})",
"IsBytesUnlimited": "= Tidak Terbatas",
"IsBytesNotANumber": "= Bukan Nomor",
-
+
"MissingVcRedist": "Visual C/C++ Redistributable Tidak Terpasang",
"MissingVcRedistSubtitle": "Kamu perlu memasang Visual C/C++ Redistributable terlebih dahulu untuk menggunakan fungsi ini. Mau unduh sekarang?\r\nCatatan: Proses ini akan membuka window browser menuju file yang perlu kamu unduh dari Microsoft. Perlu diingat bahwa kamu perlu me-restart Collapse setelah memasangnya.\r\nApabila kamu sudah memasangnya tapi error ini masih muncul, silahkan kirimkan tiket issue ke kami."
},
@@ -1148,6 +1151,10 @@
"ApplyUpdateErrCollapseRunTitle": "Mohon tutup Collapse sebelum menerapkan pembaruan!",
"ApplyUpdateErrCollapseRunSubtitle": "Menunggu Collapse untuk ditutup...",
+ "ApplyUpdateErrCollapseRunTitleWarnBox": "Sebuah Instance Collapse Launcher Masih Berjalan!",
+ "ApplyUpdateErrCollapseRunSubtitleWarnBox": "Kami menemukan sebuah instance Collapse Launcher masih berjalan di belakang. Untuk menutup secara paksa, klik \"Yes\". Untuk menunggu launcher untuk ditutup secara manual, klik \"No\".",
+ "ApplyUpdateErrVelopackStateBrokenTitleWarnBox": "Instalasi Rusak yang Sudah Ada Telah Terdeteksi!",
+ "ApplyUpdateErrVelopackStateBrokenSubtitleWarnBox": "Kami mendeteksi adanya kerusakan pada instalasi kamu yang sudah ada.\r\n\r\nKlik \"Yes\" untuk memperbaiki instalasi sebelum melakukan update, atau klik \"No\" untuk langsung menjalankan update.",
"ApplyUpdateErrReleaseFileNotFoundTitle": "ERROR:\nfile \"release\" tidak memiliki string \"stable\" atau \"preview\" didalamnya",
"ApplyUpdateErrReleaseFileNotFoundSubtitle": "Mohon periksa file \"release\" dan coba lagi.",
diff --git a/Hi3Helper.Core/Lang/ja_JP.json b/Hi3Helper.Core/Lang/ja_JP.json
index b879ff92b..806652ba3 100644
--- a/Hi3Helper.Core/Lang/ja_JP.json
+++ b/Hi3Helper.Core/Lang/ja_JP.json
@@ -150,10 +150,12 @@
"GameSettings_Panel2MoveGameLocationGame": "ゲームの位置を移動",
"GameSettings_Panel2MoveGameLocationGame_SamePath": "ゲームをドライブのルートフォルダーに移動させることはできません!\r\nフォルダーを作成してから再度お試しください。",
"GameSettings_Panel2StopGame": "ゲームを強制終了",
+ "GameSettings_Panel3RegionalSettings": "ゲームごとの設定",
"GameSettings_Panel3": "カスタム起動引数",
+ "GameSettings_Panel3RegionRpc": "Discordのステータスに表示",
"GameSettings_Panel3CustomBGRegion": "カスタム背景を選択",
"GameSettings_Panel3CustomBGRegionSectionTitle": "カスタム背景をゲームごとに変更",
- "GameSettings_Panel4": "その他",
+ "GameSettings_Panel4": "共通設定 - その他",
"GameSettings_Panel4ShowEventsPanel": "イベントパネルを表示",
"GameSettings_Panel4ShowSocialMediaPanel": "ソーシャルメディアパネルを表示",
"GameSettings_Panel4ShowPlaytimeButton": "累計プレイ時間を表示する",
@@ -747,6 +749,7 @@
"Disabled": "無効",
"Enabled": "有効",
"UseAsDefault": "デフォルトとして使用",
+ "Default": "デフォルト",
"BuildChannelPreview": "プレビュー版",
"BuildChannelStable": "安定版",
@@ -792,7 +795,7 @@
"IsBytesMoreThanBytes": "= {0} バイト (-/+ {1})",
"IsBytesUnlimited": "= 無制限",
"IsBytesNotANumber": "= 不正な数値",
-
+
"MissingVcRedist": "Visual C/C++ 再頒布可能パッケージが見つかりません",
"MissingVcRedistSubtitle": "この関数を実行するには、Visual C/C++ 再頒布可能パッケージをインストールする必要があります。今すぐダウンロードしますか?\r\n注:ブラウザを起動してMicrosoftからパッケージをダウンロードします。ダウンロード後にインストーラーを実行し、Collapseを再起動してから再度お試しください。\nインストール済みなのにエラーが解決しない場合は、Githubでissueを送信してください。"
},
@@ -1148,6 +1151,10 @@
"ApplyUpdateErrCollapseRunTitle": "アップデートを適用するにはCollapseを閉じてください!",
"ApplyUpdateErrCollapseRunSubtitle": "Collapseが閉じられるのを待っています…",
+ "ApplyUpdateErrCollapseRunTitleWarnBox": "Collapse Launcherは既に起動されています!",
+ "ApplyUpdateErrCollapseRunSubtitleWarnBox": "バックグラウンドで動作中のCollapse Launcherを検出しました。ランチャーを強制終了させたい場合は\"はい\"を押して、手動で閉じたい場合は\"いいえ\"を押してください。",
+ "ApplyUpdateErrVelopackStateBrokenTitleWarnBox": "破損した既存のインストールが検出されした!",
+ "ApplyUpdateErrVelopackStateBrokenSubtitleWarnBox": "破損した既存のインストールを検出しました。\r\n\r\n\"はい\"を押すと既存のインストールを修復してからアップデートを行い、\"いいえ\"を押すとアップデートをそのまま実行します。",
"ApplyUpdateErrReleaseFileNotFoundTitle": "エラー:\r\n\"release\"ファイルに\"stable\"か\"preview\"の記述がありません",
"ApplyUpdateErrReleaseFileNotFoundSubtitle": "\"release\"ファイルを確認してから再試行してください。",
diff --git a/Hi3Helper.Core/Lang/vi_VN.json b/Hi3Helper.Core/Lang/vi_VN.json
index 7ac2cf19d..45b03b484 100644
--- a/Hi3Helper.Core/Lang/vi_VN.json
+++ b/Hi3Helper.Core/Lang/vi_VN.json
@@ -114,7 +114,7 @@
"PageTitle": "Trình khởi chạy",
"PreloadTitle": "Tải về trước đã sẵn sàng",
"PreloadNotifTitle": "Tải trước phiên bản v{0} đã sẵn sàng!",
- "PreloadNotifDeltaDetectTitle": "Gói tải trước ở định dạng bản vá tạm thời của phiên bản v{0} đã được xác định!",
+ "PreloadNotifDeltaDetectTitle": "Gói tải trước ở định dạng bản vá tạm thờibản vá tạm thời của phiên bản v{0} đã được xác định!",
"PreloadNotifSubtitle": "Ấn \"Tải trò chơi\" để bắt đầu tải trong nền.",
"PreloadNotifDeltaDetectSubtitle": "Bạn đã thành công tải gói tài nguyên tải trước ở định dạng là bản vá tạm thời. Gói tải trước này sẽ được dùng để cập nhật trò chơi của bạn sau này.",
"PreloadNotifCompleteTitle": "Gói tải trước đã tải xong!",
@@ -150,13 +150,16 @@
"GameSettings_Panel2MoveGameLocationGame": "Chuyển vị trí thư mục Game",
"GameSettings_Panel2MoveGameLocationGame_SamePath": "Không thể di chuyển trò chơi vào gốc ổ đỉa!\r\nVui lòng tạo thư mục mới và thử lại.",
"GameSettings_Panel2StopGame": "Buộc đóng trò chơi",
+ "GameSettings_Panel3RegionalSettings": "Cài Đặt Vùng",
"GameSettings_Panel3": "Tuỳ chọn khởi động cho trò chơi",
+ "GameSettings_Panel3RegionRpc": "Hiển thị Trạng thái đang chơi trên Discord",
"GameSettings_Panel3CustomBGRegion": "Đổi nền của vùng",
"GameSettings_Panel3CustomBGRegionSectionTitle": "Hình nền tùy chỉnh cho vùng",
- "GameSettings_Panel4": "Cài đặt khác",
+ "GameSettings_Panel4": "Toàn Cầu - Khác",
"GameSettings_Panel4ShowEventsPanel": "Hiển thị bảng sự kiện",
"GameSettings_Panel4ShowSocialMediaPanel": "Hiển thị bảng mạng xã hội",
"GameSettings_Panel4ShowPlaytimeButton": "Hiển thị thời gian chơi",
+ "GameSettings_Panel4SyncPlaytimeDatabase": "Đồng bộ thời gian chơi vào cơ sở dử liệu",
"GameSettings_Panel4CreateShortcutBtn": "Tạo lối tắt",
"GameSettings_Panel4AddToSteamBtn": "Thêm vào Steam",
@@ -165,15 +168,25 @@
"GamePlaytime_Idle_Panel1Minutes": "Phút",
"GamePlaytime_Idle_ResetBtn": "Đặt lại",
"GamePlaytime_Idle_ChangeBtn": "Thay đổi",
+ "GamePlaytime_Idle_SyncDb": "Đồng bộ",
+ "GamePlaytime_Idle_SyncDbSyncing": "Đang Đồng Bộ",
"GamePlaytime_Running_Info1": "Tiến trình của trò chơi này hiện đang chạy, do đó không thể chỉnh sửa thời gian chơi.",
- "GamePlaytime_Running_Info2": "Xin lưu ý rằng việc đóng hoàn toàn Collapse sẽ tạm dừng theo dõi thời gian chơi (thời gian chơi cho đến thời điểm đóng lại) & Collapse sẽ tiếp tục theo dõi khi được mở lại.",
+ "GamePlaytime_Running_Info2": "Lưu ý rằng việc đóng hoàn toàn Collapse sẽ ngừng theo dõi thời gian chơi (lưu thời gian đã chơi cho đến thời điểm đó).",
"GamePlaytime_Display": "{0}h {1}m",
- "GamePlaytime_DateDisplay": "{3:00}:{4:00} {0:00}/{1:00}/{2:0000}",
+ "GamePlaytime_DateDisplay": "{0:00}/{1:00}/{2:0000} {3:00}:{4:00}",
+ "GamePlaytime_Stats_Title": "Thống kê thời gian Chơi",
+ "GamePlaytime_Stats_NeverPlayed": "Chưa bao giờ chơi",
+ "GamePlaytime_Stats_LastSession": "Lần chơi cuối cùng",
+ "GamePlaytime_Stats_LastSession_StartTime": "Thời gian bắt đầu",
+ "GamePlaytime_Stats_LastSession_Duration": "Khoảng thời gian",
+ "GamePlaytime_Stats_Daily": "Hôm nay",
+ "GamePlaytime_Stats_Weekly": "Tuần",
+ "GamePlaytime_Stats_Monthly": "Tháng",
"PostPanel_Events": "Sự kiện",
"PostPanel_Notices": "Thông báo",
"PostPanel_Info": "Thông tin",
- "PostPanel_NoNews": "Gì?\nKhông có bản tin mới trong ngày hôm nay?",
+ "PostPanel_NoNews": "\nKhông có bản tin mới trong ngày hôm nay?",
"CommunityToolsBtn": "Công cụ cộng đồng",
"CommunityToolsBtn_OfficialText": "Công cụ chính thức",
@@ -278,7 +291,7 @@
},
"_CutscenesPage": {
- "PageTitle": "Hoạt Cảnh"
+ "PageTitle": "Cutscenes"
},
"_GameSettingsPage": {
@@ -387,14 +400,14 @@
"CustomArgs_Title": "Cấu hình tham số khởi động (Nâng cao)",
"CustomArgs_Subtitle": "Tham số khởi động",
"CustomArgs_Footer1": "Về các tham số khởi động, vui lòng xem tại",
- "CustomArgs_Footer2": "Tùy chọn dòng lệnh cho Unity Standalone Player",
+ "CustomArgs_Footer2": "Unity Standalone Command-line documentation",
"CustomArgs_Footer3": "để biết thêm chi tiết.",
"GameBoost": "Tăng mức độ ưu tiên của trò chơi [Thử nghiệm]",
"MobileLayout": "Sử dụng Layout Cảm Ứng",
"Advanced_Title": "Cài đặt Nâng cao",
- "Advanced_Subtitle1": "Nhóm Collapse Launcher",
+ "Advanced_Subtitle1": "Collapse Launcher Team",
"Advanced_Subtitle2": "KHÔNG CHỊU TRÁCH NHIỆM",
"Advanced_Subtitle3": "cho bất kỳ điều gì xảy đến với trò chơi, tài khoản hay hệ thống của bạn khi sử dụng thiết đặt này! Cân nhắc khi sử dụng.",
"Advanced_GLC_WarningAdmin": "CẢNH BÁO: Lệnh được điền vào sẽ được chạy với quyền quản trị!",
@@ -412,6 +425,8 @@
"Debug": "Cài đặt bổ sung",
"Debug_Console": "Hiển thị console (cmd)",
"Debug_IncludeGameLogs": "Lưu nhật ký trò chơi vào Collapse (có thể chứa dữ liệu nhạy cảm)",
+ "Debug_SendRemoteCrashData": "Gửi báo cáo sự cố ẩn danh cho nhà phát triển",
+ "Debug_SendRemoteCrashData_EnvVarDisablement": "Cài đặt này bị tắt do biến môi trường 'DISABLE_SENTRY' được đặt thành true.",
"Debug_MultipleInstance": "Cho phép chạy nhiều phiên bản Collapse",
"ChangeRegionWarning_Toggle": "Hiển thị thông báo thay đổi vùng",
@@ -444,7 +459,7 @@
"AppThreads": "Tải về",
"AppThreads_Download": "Số lượng tải về",
"AppThreads_Extract": "Số lượng giải nén",
- "AppThreads_Help1": "Tuỳ chọn này sẽ chọn số lượng tệp mà trình khởi chạy sẽ tải về. Cách thức của nó giống IDM hoặc Parallel download trên Chrome",
+ "AppThreads_Help1": "Tuỳ chọn này sẽ chọn số lượng tệp mà trình khởi chạy sẽ tải về. Cách thức của nó giống IDM hoặc Parallel download trên chrome",
"AppThreads_Help2": "Giá trị mặc định:",
"AppThreads_Help3": "Khoảng giá trị:",
"AppThreads_Help4": "0 (Tự động)",
@@ -464,6 +479,8 @@
"SophonHttpNumberBox": "Số lượng kết nối HTTP tối đa",
"SophonHelp_Http": "Tùy chọn này kiểm soát số lượng kết nối được thiết lập bởi Collapse tới máy chủ cặp nhật của HoYoVerse để có thể tải những khối dữ liệu nhỏ.",
"SophonToggle": "Kích hoạt Sophon cho những vùng hỗ trợ",
+ "SophonPredownPerfMode_Toggle": "[THỬ NGHIỆM] Sử dụng tất cả nhân CPU khi áp dụng Cài đặt Trước",
+ "SophonPredownPerfMode_Tooltip": "Kích hoạt tính năng này sẽ dùng tất cả luồng CPU có trong hệ thống. Vô hiệu hóa tính năng này nếu có vấn đề",
"AppThreads_Attention": "Chú ý",
"AppThreads_Attention1": "Trước khi bạn thay đổi giá trị",
@@ -480,8 +497,10 @@
"DiscordRPC_GameStatusToggle": "Hiện trò chơi hiện tại trên trạng thái của Discord",
"DiscordRPC_IdleStatusToggle": "Hiện RPC khi ở trạng thái chờ",
+ "ImageBackground": "Cài đặt ảnh nền",
"VideoBackground": "Cài đặt video",
"VideoBackground_IsEnableAudio": "Bật âm thanh",
+ "VideoBackground_IsEnableAcrylicBackground": "Sử dụng hiệu ứng Acrylic khi sử dụng Video nền",
"VideoBackground_AudioVolume": "Âm lượng",
"Update": "Cập nhật",
@@ -521,7 +540,7 @@
"Disclaimer1": "trình khởi chạy này không thuộc về",
"Disclaimer2": "với bất kì điều nào",
"Disclaimer3": "và hoàn toàn là mã nguồn mở. Chúng tôi luôn vui lòng với bất kì sự đóng góp nào đến từ bạn.",
-
+ "WebsiteBtn": "Truy cập trang web của chúng tôi",
"DiscordBtn1": "Tham gia Armada Discord của chúng tôi",
"DiscordBtn2": "Xem qua Discord Honkai Impact 3rd",
"DiscordBtn3": "Discord Chính thức của Collapse!",
@@ -530,6 +549,7 @@
"EnableAcrylicEffect": "Dùng hiệu ứng làm mờ",
"EnableDownloadChunksMerging": "Gộp các gói đã tải xuống",
+ "Enforce7ZipExtract": "Luôn sử dụng 7-zip cho cài đặt trò chơi/cặp nhật",
"UseExternalBrowser": "Luôn dùng trình duyệt bên ngoài",
"LowerCollapsePrioOnGameLaunch": "Giảm mức sử dụng tài nguyên sử dụng của Collapse khi trò chơi được khởi động",
@@ -548,7 +568,7 @@
"AppBehavior_LaunchOnStartup": "Luôn mở Collapse lúc máy tính của bạn khởi động",
"AppBehavior_StartupToTray": "Ẩn cửa sổ Collapse lúc nó tự mở",
- "Waifu2X_Toggle": "Dùng Waifu2X [Thử nghiệm]",
+ "Waifu2X_Toggle": "Dùng Waifu2X",
"Waifu2X_Help": "Dùng Waifu2X để phóng lớn ảnh nền.\nKhi bật, chất lượng ảnh sẽ được cải thiện đáng kể, nhưng sẽ mất thêm một ít thời gian để tải ảnh lần đầu.",
"Waifu2X_Help2": "Chỉ khả dụng cho ảnh tĩnh!",
"Waifu2X_Warning_CpuMode": "CẢNH BÁO: Không tìm thấy thiết bị GPU Vulcan có sẵn và sẽ sử dụng chế độ CPU. Điều này sẽ làm tăng đáng kể thời gian xử lý hình ảnh.",
@@ -622,7 +642,25 @@
"FileDownloadSettings_BurstDownloadHelp4": "chạy song song để làm cho trình tải xuống hiệu quả hơn.",
"FileDownloadSettings_BurstDownloadHelp5": "Khi vô hiệu hóa, trình tải xuống cho tính năng",
"FileDownloadSettings_BurstDownloadHelp6": "và",
- "FileDownloadSettings_BurstDownloadHelp7": "dùng trình tải xuống nối tiếp."
+ "FileDownloadSettings_BurstDownloadHelp7": "dùng trình tải xuống nối tiếp.",
+
+ "Database_Title": "Cơ sở dữ liệu có thể đồng bọ hóa của người dùng",
+ "Database_ConnectionOk": "Đã kết nối cơ sở dữ liệu",
+ "Database_ConnectFail": "Không thể kết nối với cơ sở dữ liệu, xem (các) lỗi bên dưới:",
+ "Database_Toggle": "Bật Cở Sở Dử Liệu Trực tuyến",
+ "Database_Url": "URL Cở sở Dử liệu",
+ "Database_Url_Example": "Ví dụ: https://db-collapse.turso.io",
+ "Database_Token": "Mã Token",
+ "Database_UserId": "ID Người dùng",
+ "Database_GenerateGuid": "Tạo UID",
+ "Database_Validate": "Xác thực và lưu cài đặt",
+ "Database_Error_EmptyUri": "URL Cơ sở Dử liệu không được để trống",
+ "Database_Error_EmptyToken": "Mã token không được để trống",
+ "Database_Error_InvalidGuid": "ID người dùng không đúng với GUID!",
+ "Database_Warning_PropertyChanged": "Cơ sở dữ liệu đã thay đổi",
+ "Database_ValidationChecking": "Xác thực cài đặt",
+ "Database_Placeholder_DbUserIdTextBox": "Ví dụ về GUID: ed6e8048-e3a0-4983-bd56-ad19956c701f",
+ "Database_Placeholder_DbTokenPasswordBox": "Nhập token của trình xác thực tại đây"
},
"_Misc": {
@@ -642,6 +680,7 @@
"PerFromTo": "{0} / {1}",
"PerFromToPlaceholder": "- / -",
+ "EverythingIsOkay": "Tất cả OK!",
"Cancel": "Huỷ",
"Close": "Đóng",
"UseCurrentDir": "Dùng thư mục hiện tại",
@@ -710,6 +749,7 @@
"Disabled": "Tắt",
"Enabled": "Bật",
"UseAsDefault": "Đặt Làm Mặc Định",
+ "Default": "Mặc định",
"BuildChannelPreview": "Xem trước",
"BuildChannelStable": "Ổn định",
@@ -754,7 +794,10 @@
"IsBytesMoreThanBytes": "= {0} bytes (-/+ {1})",
"IsBytesUnlimited": "= Không giới hạn",
- "IsBytesNotANumber": "= NaN"
+ "IsBytesNotANumber": "= NaN",
+
+ "MissingVcRedist": "Thiếu Visual C/C++ Redistributable",
+ "MissingVcRedistSubtitle": "Bạn cần phải tải Visual C/C++ Redistributable để có thể chạy tính năng này. Bạn có muốn tải ngay bây giờ không?\r\nLưu ý: Thao tác này sẽ mở một cửa sổ trình duyệt và tải xuống tệp từ Microsoft. Vui lòng chạy trình cài đặt sau khi tải xuống rồi khởi động lại Collapse và thử lại.\r\nNếu bạn đã cài đặt nó nhưng vẫn còn lỗi, vui lòng gửi phản hồi về vấn đề này cho chúng tôi."
},
"_BackgroundNotification": {
@@ -801,7 +844,7 @@
"RepairCompletedSubtitle": "{0} tệp đã được sửa.",
"RepairCompletedSubtitleNoBroken": "Không có tệp nào lỗi.",
"ExtremeGraphicsSettingsWarnTitle": "Đã chọn tuỳ chọn đồ hoạ cao nhất!",
- "ExtremeGraphicsSettingsWarnSubtitle": "Bạn đã đặt độ hoạ ở mức rất cao!\r\nĐồ hoạ cao cũng đồng nghĩa số lần render cao gấp đôi với MSAA được bật và nó CHƯA ĐƯỢC TỐI ƯU! (trừ khi bạn sài siêu máy tính của NASA).\r\n\r\nBạn có muốn tiếp tục chứ?",
+ "ExtremeGraphicsSettingsWarnSubtitle": "Bạn sặp đặt cài đặt cài sẵn thành Rất Cao!\r\nCài đặt Rất Cao về cơ bản là cài đặt cài sẵn chỉnh tỉ lệ kết xuất lên 1.6 lần với MSAA kích hoạt và RẤT KHÔNG TỐI ƯU!\r\n\r\nBạn có chắc là bạn muốn làm điều này?",
"MigrateExistingMoveDirectoryTitle": "Di chuyển Cài đặt Hiện tại cho: {0}",
"MigrateExistingInstallChoiceTitle": "Một Cài đặt Hiện tại của {0} đã được Phát hiện!",
"MigrateExistingInstallChoiceSubtitle1": "Bạn có một phiên bản cài đặt hiện tại của trò chơi sử dụng {0} trong thư mục này:",
@@ -866,7 +909,7 @@
"ChangePlaytimeTitle": "Bạn có chắc chắn muốn thay đổi thời gian chơi của mình không?",
"ChangePlaytimeSubtitle": "Thay đổi thời gian chơi của bạn sẽ ghi đè giá trị hiện tại bằng giá trị bạn vừa nhập.\n\nBạn có muốn tiếp tục?\n\nLưu ý: Điều này không ảnh hưởng đến cách Collapse hoạt động và bạn có thể thay đổi lại giá trị này bất kỳ lúc nào khi không chơi trò chơi.",
"ResetPlaytimeTitle": "Bạn có chắc muốn đặt lại thời gian chơi?",
- "ResetPlaytimeSubtitle": "Đặt lại thời gian chơi sẽ đặt bộ đếm thời gian về 0. Đây là ",
+ "ResetPlaytimeSubtitle": "Đặt lại thời gian chơi của là đặt lại bộ đếm thời gian chơi và thống kê liên quan về lại 0. Đây là một",
"ResetPlaytimeSubtitle2": "phá hoại",
"ResetPlaytimeSubtitle3": " hành động, nghĩa là bạn không thể hoàn tác thao tác này sau khi đã xác nhận. \n\nBạn có muốn tiếp tục?\n\nLưu ý: Điều này không ảnh hưởng đến cách Collapse hoạt động và bạn có thể thay đổi lại giá trị này bất kỳ lúc nào khi không chơi trò chơi.",
"InvalidPlaytimeTitle": "Đã có lỗi khi lưu thời gian chơi của lần chơi này",
@@ -879,6 +922,22 @@
"CannotUseAppLocationForGameDirTitle": "Thư mục không hợp lệ!",
"CannotUseAppLocationForGameDirSubtitle": "Bạn không thể sử dụng thư mục này vì nó đang được sử dụng làm thư mục hệ thống hoặc được sử dụng để thực thi chính của ứng dụng. Vui lòng chọn thư mục khác!",
+ "InvalidGameDirNewTitleFormat": "Đường dẫn sai: {0}",
+ "InvalidGameDirNewSubtitleSelectedPath": "Đã chọn đường dẫn:",
+ "InvalidGameDirNewSubtitleSelectOther": "Hãy chọn thư mục/vị trí khác:",
+ "InvalidGameDirNew1Title": "Thư mục không hợp lệ!",
+ "InvalidGameDirNew1Subtitle": "Bạn không thể sử dụng thư mục này vì nó đang được sử dụng làm thư mục hệ thống hoặc được sử dụng để thực thi chính cho ứng dụng. Vui lòng chọn thư mục khác!",
+ "InvalidGameDirNew2Title": "Không thể truy cập vào thư mục được chọn",
+ "InvalidGameDirNew2Subtitle": "Trình khởi chạy không có quyền truy cập vào thư mục này",
+ "InvalidGameDirNew3Title": "Không thể chọn thư mục gốc",
+ "InvalidGameDirNew3Subtitle": "Bạn đã chọn một đường dẫn trên thư mục gốc, và điều này bị cấm!",
+ "InvalidGameDirNew4Title": "Không thể chọn thư mục Windows",
+ "InvalidGameDirNew4Subtitle": "Bạn không thể sử dụng thư mục Windows làm vị trí cài đặt trò chơi để tránh mọi điều không cần thiết có thể xảy ra.",
+ "InvalidGameDirNew5Title": "Không thể chọn thư mục Program Data",
+ "InvalidGameDirNew5Subtitle": "Bạn không thể sử dụng thư mục Program Data làm vị trí cài đặt trò chơi để tránh mọi điều không cần thiết có thể xảy ra.",
+ "InvalidGameDirNew6Title": "Không thể chọn thư mục Program Files hay Program Files (x86)",
+ "InvalidGameDirNew6Subtitle": "Bạn không thể sử dụng thư mục Program Files hay Program Files (x86) làm vị trí cài đặt trò chơi để tránh mọi điều không cần thiết có thể xảy ra.",
+ "FolderDialogTitle1": "Chọn vị trí cài trò chơi",
"StopGameTitle": "Buộc dừng trò chơi",
"StopGameSubtitle": "Bạn có chắc chắn muốn buộc dừng trò chơi đang chạy hiện tại không?\nBạn có thể mất một số tiến trình trong trò chơi.",
@@ -933,7 +992,10 @@
"DownloadSettingsOption1": "Khởi động trò chơi sau khi cài đặt",
"OpenInExternalBrowser": "Mở trong trình duyệt",
- "CloseOverlay": "Đóng cửa sổ phủ"
+ "CloseOverlay": "Đóng cửa sổ phủ",
+
+ "DbGenerateUid_Title": "Bạn có chắc là bạn có muốn đổi ID người dùng không?",
+ "DbGenerateUid_Content": "Việc thay đổi ID người dùng hiện tại sẽ khiến dữ liệu liên quan bị mất nếu bạn làm mất."
},
"_FileMigrationProcess": {
@@ -1089,6 +1151,10 @@
"ApplyUpdateErrCollapseRunTitle": "Vui lòng đóng Collapse trước khi áp dụng cập nhật!",
"ApplyUpdateErrCollapseRunSubtitle": "Đang chờ Collapse đóng...",
+ "ApplyUpdateErrCollapseRunTitleWarnBox": "Một phiên bản của Collapse Launcher vẫn đang chạy!",
+ "ApplyUpdateErrCollapseRunSubtitleWarnBox": "Chúng tôi đã phát hiện một phiên bản của Collapse Launcher đang chạy trong nền. Để buộc đóng trình khởi chạy, hãy nhấp vào \"Có\". Để đợi cho đến khi bạn đóng nó theo cách thủ công, hãy nhấp vào \"Không\".",
+ "ApplyUpdateErrVelopackStateBrokenTitleWarnBox": "Đã phát hiện cài đặt hiện có bị hỏng!",
+ "ApplyUpdateErrVelopackStateBrokenSubtitleWarnBox": "Chúng tôi đã phát hiện ra rằng bạn có một cài đặt hiện có bị hỏng.\r\n\r\nNhấp vào \"Có\" để sửa chữa cài đặt trước khi cài đặt bản cập nhật hoặc Nhấp vào \"Không\" để chỉ chạy cài đặt cập nhật.",
"ApplyUpdateErrReleaseFileNotFoundTitle": "LỖI:\r\ntệp \"release\" không có string \"stable\" hay \"preview\" trong nó",
"ApplyUpdateErrReleaseFileNotFoundSubtitle": "Vui lòng kiểm tra tệp \"release\" của bạn và thử lại.",
@@ -1299,7 +1365,7 @@
"Audio_Output_Surround": "Âm thanh vòm",
"Audio_DynamicRange": "Đầy Đủ Dynamic Range",
- "Audio_MuteOnMinimize": "Tắt âm thanh khi ẩn",
+ "Audio_MuteOnMinimize": "Mute Audio When Minimized",
"Language": "Cài Đặt Ngôn Ngữ",
"Language_Help1": "Collapse hiện không thể tải trực tiếp gói ngôn ngữ.",
@@ -1392,6 +1458,8 @@
"LoadingTitle": "Đang xử lý",
"LoadingSubtitle1": "Tính toán các tập hiện có (tìm thấy {0} tệp - tổng cộng {1})",
"LoadingSubtitle2": "Kiểm tra tính khả dụng của pkg_version",
+ "LoadingSubtitle3": "Giao diện có thể không phản hồi trong quá trình này...",
+ "DeleteSubtitle": "Đang xoá tập tin",
"BottomButtonDeleteAllFiles": "Xóa tất cả tệp",
"BottomButtonDeleteSelectedFiles": "Xóa {0} (các) tệp đã chọn",
"BottomCheckboxFilesSelected": "{0} tệp đã chọn (Tổng cộng {1} / {2})",
@@ -1453,6 +1521,7 @@
"Graphics_EffectsQ": "Chất lượng hiệu ứng",
"Graphics_ShadingQ": "Chất lượng bóng",
"Graphics_Distortion": "Bóp méo",
+ "Graphics_HighPrecisionCharacterAnimation": "Hoạt Ảnh Nhân Vật có độ chính xác cao",
"Audio_PlaybackDev": "Thiết bị phát",
"Audio_PlaybackDev_Headphones": "Tai Nghe",
"Audio_PlaybackDev_Speakers": "Loa",
@@ -1485,6 +1554,12 @@
"CacheUpdateDownloadCompleted_Title": "Tải xuống tệp Cache đã hoàn tất.",
"CacheUpdateDownloadCompleted_Subtitle": "{0} tệp cache đã được cập nhật thành công.",
- "GenericClickNotifToGoBack_Subtitle": "Nhấn vào thông báo này để quay lại trình khởi chạy trò chơi."
+ "GenericClickNotifToGoBack_Subtitle": "Nhấn vào thông báo này để quay lại trình khởi chạy trò chơi.",
+
+ "OOBE_WelcomeTitle": "Chào mừng bạn tới Collapse Launcher!",
+ "OOBE_WelcomeSubtitle": "Bạn hiện đang chọn {0} - {1} là trò chơi của bạn. Còn rất nhiều trò chơi đợi bạn, khám phá thêm!",
+
+ "LauncherUpdated_NotifTitle": "Trình khởi chạy của bạn đã được cập nhật!",
+ "LauncherUpdated_NotifSubtitle": "Trình khởi chạy của bạn đã được cập nhật đến phiên bản: {0}. Hãy vào {1} và chọn {2} để xem những gì đẫ thay đổi."
}
}
diff --git a/Hi3Helper.Core/Lang/zh_CN.json b/Hi3Helper.Core/Lang/zh_CN.json
index 6ed4bcd71..b88d2ca08 100644
--- a/Hi3Helper.Core/Lang/zh_CN.json
+++ b/Hi3Helper.Core/Lang/zh_CN.json
@@ -116,16 +116,16 @@
"PreloadNotifTitle": "v{0} 的预载包可用!",
"PreloadNotifDeltaDetectTitle": "检测到 v{0} 的 Delta Patch 格式的预载包!",
"PreloadNotifSubtitle": "点击“下载游戏”开始在后台下载。",
- "PreloadNotifDeltaDetectSubtitle": "您已经成功地预加载了 Delta Patch 格式的补丁。这种预加载格式将稍后用于更新您的游戏。",
+ "PreloadNotifDeltaDetectSubtitle": "您已经成功预加载了 Delta Patch 格式的补丁。这种预加载格式将稍后用于更新您的游戏。",
"PreloadNotifCompleteTitle": "预载包已下载!",
"PreloadNotifCompleteSubtitle": "您已经成功预下载了 v{0} 的更新包!",
"PreloadNotifIntegrityCheckBtn": "验证完整性",
"StartBtn": "开始游戏",
"StartBtnRunning": "游戏正在运行!",
- "VerifyingPkgTitle": "验证软件包",
+ "VerifyingPkgTitle": "正在验证软件包",
"PreloadDownloadNotifbarTitle": "正在下载预载包",
"PreloadDownloadNotifbarVerifyTitle": "正在验证预载包",
- "PreloadDownloadNotifbarSubtitle": "必要的软件包",
+ "PreloadDownloadNotifbarSubtitle": "必要软件包",
"UpdatingVoicePack": "正在更新语音包……",
"InstallBtn": "安装/定位游戏",
"UpdateBtn": "更新游戏",
@@ -150,10 +150,12 @@
"GameSettings_Panel2MoveGameLocationGame": "移动游戏位置",
"GameSettings_Panel2MoveGameLocationGame_SamePath": "无法将游戏移动到硬盘根目录!\r\n请新建一个文件夹并重试。",
"GameSettings_Panel2StopGame": "强制关闭游戏",
+ "GameSettings_Panel3RegionalSettings": "区域设置",
"GameSettings_Panel3": "自定义启动参数",
+ "GameSettings_Panel3RegionRpc": "在 Discord 上展示游玩状态",
"GameSettings_Panel3CustomBGRegion": "修改区服背景",
"GameSettings_Panel3CustomBGRegionSectionTitle": "自定义区服背景",
- "GameSettings_Panel4": "杂项",
+ "GameSettings_Panel4": "全局 - 杂项",
"GameSettings_Panel4ShowEventsPanel": "显示公告面板",
"GameSettings_Panel4ShowSocialMediaPanel": "显示社交媒体面板",
"GameSettings_Panel4ShowPlaytimeButton": "显示游戏时间",
@@ -669,8 +671,8 @@
"FeatureUnavailableTitle": "此功能暂时不可用",
"FeatureUnavailableSubtitle": "请稍候再试!",
"TimeRemain": "剩余时间",
- "TimeRemainHMSFormat": "还剩{0:%h}时{0:%m}分{0:%s}秒",
- "TimeRemainHMSFormatPlaceholder": "还剩--时--分--秒",
+ "TimeRemainHMSFormat": "{0:%h}时 {0:%m}分 {0:%s}秒",
+ "TimeRemainHMSFormatPlaceholder": "--时 --分 --秒",
"Speed": "速度:{0}/s",
"SpeedTextOnly": "速度",
"SpeedPerSec": "{0}/s",
@@ -747,6 +749,7 @@
"Disabled": "关闭",
"Enabled": "开启",
"UseAsDefault": "设为默认",
+ "Default": "默认",
"BuildChannelPreview": "预览版",
"BuildChannelStable": "正式版",
@@ -792,7 +795,7 @@
"IsBytesMoreThanBytes": "= {0} 字节 (-/+ {1})",
"IsBytesUnlimited": "= 无限制",
"IsBytesNotANumber": "= NaN",
-
+
"MissingVcRedist": "缺少 Visual C/C++ 可再发行程序包",
"MissingVcRedistSubtitle": "您需要安装 Visual C/C++ 可再发行程序包才能运行此功能。您现在要下载吗?\n注意:这将打开一个浏览器窗口,并从微软下载一个文件。请下载后运行安装程序,然后重新启动 Collapse 并重试。\n如果您已经安装过了但仍然报错,请在 GitHub 上给我们提交一个 issue。"
},
@@ -825,7 +828,7 @@
"GameConversionPrevFailedTitle": "先前的游戏转换失败或未完成!",
"GameConversionPrevFailedSubtitle": "先前的游戏转换失败。您想要还原游戏以防止重新下载吗?",
"PreloadVerifiedTitle": "预载包已验证!",
- "PreloadVerifiedSubtitle": "您的预载包已准备好并已验证!",
+ "PreloadVerifiedSubtitle": "您的预载包已完成验证并做好安装准备!",
"LocateInstallTitle": "找到安装文件夹",
"LocateInstallSubtitle": "在安装游戏之前,您是否要指定游戏的位置?",
"UnauthorizedDirTitle": "选择到未经授权的位置",
@@ -1148,6 +1151,10 @@
"ApplyUpdateErrCollapseRunTitle": "请在应用更新前关闭 Collapse!",
"ApplyUpdateErrCollapseRunSubtitle": "等待 Collapse 关闭……",
+ "ApplyUpdateErrCollapseRunTitleWarnBox": "另一个 Collapse 启动器实例仍在运行中!",
+ "ApplyUpdateErrCollapseRunSubtitleWarnBox": "我们检测到在后台有一个正在运行中的 Collapse 启动器实例。要强制关闭这个启动器,点击“是”;若要等待手动关闭,点击“否”。",
+ "ApplyUpdateErrVelopackStateBrokenTitleWarnBox": "检测到损坏的现有安装!",
+ "ApplyUpdateErrVelopackStateBrokenSubtitleWarnBox": "我们检测到您有一个损坏的现有的安装。\n\n点击“是”以在安装更新前修复安装,或点击“否”直接安装更新。",
"ApplyUpdateErrReleaseFileNotFoundTitle": "错误:\r\n\"release\" 文件中没有 \"stable\" 或 \"preview\" 字符串",
"ApplyUpdateErrReleaseFileNotFoundSubtitle": "请检查您的 \"release\" 文件并重试。",
diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool
index e07511e7e..d7b49a16a 160000
--- a/Hi3Helper.EncTool
+++ b/Hi3Helper.EncTool
@@ -1 +1 @@
-Subproject commit e07511e7ed24bd2e0222e226bf4963440cd02045
+Subproject commit d7b49a16a91620769d43da5868c30a8423ca594e
diff --git a/Hi3Helper.Sophon b/Hi3Helper.Sophon
index b2367ac38..01d7af7a3 160000
--- a/Hi3Helper.Sophon
+++ b/Hi3Helper.Sophon
@@ -1 +1 @@
-Subproject commit b2367ac38c4e89dcf3b2775782b4c4745631b107
+Subproject commit 01d7af7a3c0bd654ae44a1852f5e3bb867f5a63a
diff --git a/Hi3Helper.Win32 b/Hi3Helper.Win32
index b0cbcfc9f..7ea65c100 160000
--- a/Hi3Helper.Win32
+++ b/Hi3Helper.Win32
@@ -1 +1 @@
-Subproject commit b0cbcfc9f22871cc8f090b9c30ac1fd5825c7520
+Subproject commit 7ea65c100f99e716cbf726364f46095835f4d3e8
diff --git a/README.md b/README.md
index ba62e1eee..9dd24ebfc 100644
--- a/README.md
+++ b/README.md
@@ -229,11 +229,11 @@ Not only that, this launcher also has some advanced features for **Genshin Impac
> > Please keep in mind that the Game Conversion feature is currently only available for Honkai Impact: 3rd. Other miHoYo/Cognosphere Pte. Ltd. games are currently not planned for game conversion.
# Download Ready-To-Use Builds
-[](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.82.9/CollapseLauncher-stable-Setup.exe)
-> **Note**: The version for this build is `1.82.9` (Released on: December 25th, 2024).
+[](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.82.10/CollapseLauncher-stable-Setup.exe)
+> **Note**: The version for this build is `1.82.10` (Released on: December 27th, 2024).
-[](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.82.9-pre/CollapseLauncher-preview-Setup.exe)
-> **Note**: The version for this build is `1.82.9` (Released on: December 25th, 2024).
+[](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.82.10-pre/CollapseLauncher-preview-Setup.exe)
+> **Note**: The version for this build is `1.82.10` (Released on: December 27th, 2024).
To view all releases, [**click here**](https://github.com/neon-nyan/CollapseLauncher/releases).
diff --git a/appveyor.yml b/appveyor.yml
index bba443372..d0df22fb8 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -28,7 +28,7 @@ for:
- cmd: >-
echo Init submodules
- git submodule update --init --force --depth=20 --recursive
+ git submodule update --init --force --depth=20 --recursive --jobs=4
echo Install dotnet sdk
@@ -133,7 +133,7 @@ for:
- cmd: >-
echo Init submodules
- git submodule update --init --force --depth=20 --recursive
+ git submodule update --init --force --depth=20 --recursive --jobs=4
echo.