diff --git a/.github/ISSUE_TEMPLATE/CHS-bug-report.yml b/.github/ISSUE_TEMPLATE/CHS-bug-report.yml index 0123844f4b..b2165ac407 100644 --- a/.github/ISSUE_TEMPLATE/CHS-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/CHS-bug-report.yml @@ -1,7 +1,8 @@ name: 问题反馈 description: 通过这个议题向开发团队反馈你发现的程序中的问题 title: "[Bug]: 在这里填写一个合适的标题" -labels: ["BUG", "priority:none"] +type: "Bug" +labels: ["priority:none"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/CHS-feature-request.yml b/.github/ISSUE_TEMPLATE/CHS-feature-request.yml index 6c443b0a23..06ba93500d 100644 --- a/.github/ISSUE_TEMPLATE/CHS-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/CHS-feature-request.yml @@ -1,7 +1,8 @@ name: 功能请求 description: 通过这个议题来向开发团队分享你的想法 title: "[Feat]: 在这里填写一个合适的标题" -labels: ["feature request", "needs-triage", "priority:none"] +type: "Feature" +labels: ["needs-triage", "priority:none"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/CHS-network-issue.yml b/.github/ISSUE_TEMPLATE/CHS-network-issue.yml index 8576da0f3f..cbb96bf03b 100644 --- a/.github/ISSUE_TEMPLATE/CHS-network-issue.yml +++ b/.github/ISSUE_TEMPLATE/CHS-network-issue.yml @@ -1,6 +1,7 @@ name: 网络问题 description: 通过这个议题来反馈网络问题 title: "[Network]: 在这里填写一个合适的标题" +type: "Bug" labels: ["area-Network"] assignees: - Lightczx diff --git a/.github/ISSUE_TEMPLATE/ENG-bug-report.yml b/.github/ISSUE_TEMPLATE/ENG-bug-report.yml index 3576e416b3..68ad9d1204 100644 --- a/.github/ISSUE_TEMPLATE/ENG-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/ENG-bug-report.yml @@ -1,7 +1,8 @@ name: BUG Report [English Form] description: Tell us what issue you get title: "[ENG][Bug]: Place your Issue Title Here" -labels: ["BUG", "priority:none"] +type: "Bug" +labels: ["priority:none"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/ENG-feature-request.yml b/.github/ISSUE_TEMPLATE/ENG-feature-request.yml index 591e9cc7b9..e7ebcae607 100644 --- a/.github/ISSUE_TEMPLATE/ENG-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/ENG-feature-request.yml @@ -1,7 +1,8 @@ name: Feature Request [English Form] description: Tell us about your thought title: "[Feat]: Place your title here" -labels: ["feature request", "needs-triage", "priority:none"] +type: "Feature" +labels: ["needs-triage", "priority:none"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/ENG-network-issue.yml b/.github/ISSUE_TEMPLATE/ENG-network-issue.yml index 5d46323aaa..13d491f058 100644 --- a/.github/ISSUE_TEMPLATE/ENG-network-issue.yml +++ b/.github/ISSUE_TEMPLATE/ENG-network-issue.yml @@ -1,6 +1,7 @@ name: Network Issue [English Form] description: Submit this issue form when network issue affect your client experience title: "[Network]: Place your title here" +type: "Bug" labels: ["area-Network"] assignees: - Lightczx diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index e7a98c85a1..f2b7fc3bfc 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -99,3 +99,13 @@ jobs: " echo $summary >> $Env:GITHUB_STEP_SUMMARY + + - name: Clean up + run: | + Write-Host "Cleaning up NuGet cache..." + Remove-Item -Recurse -Force "$env:USERPROFILE\.nuget\packages" + Write-Host "NuGet cache cleaned." + + Write-Host "Cleaning up .NET install folder..." + Remove-Item -Recurse -Force "C:\Users\Public\Documents\dotnet_install" + Write-Host ".NET install folder cleaned." diff --git a/README.md b/README.md index c178c87055..3c750cab77 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,10 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio Snap Hutao is currently using sponsored software from the following service providers. -| [![](https://www.netlify.com/v3/img/components/netlify-light.svg)](https://www.netlify.com/) | [![](https://support.crowdin.com/images/logos/core-logo/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://github.com/user-attachments/assets/1fe09f87-3f56-4341-8a34-5ed8151f5f10)](https://navicat.com) | -| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | -| [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://www.digitalocean.com) | -| [![ducalis](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/ducalis.svg)](https://hi.ducalis.io/) | [![jetbrains](https://github.com/DGP-Studio/Snap.Hutao/assets/36357191/4105772a-728a-4a84-9c6e-d713a5698a20)](https://www.jetbrains.com/opensource/) | | +| [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/netlify.svg)](https://www.netlify.com/) | [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/navicat.svg)](https://navicat.com/) | +| :----------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------: | +| [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/signpath.svg)](https://about.signpath.io) | [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/1Password.svg)](https://1password.com/) | [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/digitalocean.svg)](https://www.digitalocean.com) | +| [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/ducalis.svg)](https://hi.ducalis.io/) | [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/jetbrains.svg)](https://www.jetbrains.com/) | [![](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/termius-logo.svg)](https://termius.com) | - Netlify provides document and home page hosting service for Snap Hutao diff --git a/appveyor.yml b/appveyor.yml index afe74bc290..4bb093f9e7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,8 @@ branches: - "release" build_cloud: HUTAO-ACTIONS image: Visual Studio 2022 +cache: + - 'C:\Users\Public\Documents\dotnet_install\.nuget\packages' clone_depth: 3 clone_folder: C:\Users\Public\appveyor\Snap.Hutao install: @@ -18,3 +20,12 @@ deploy: url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=msix authorization: secure: j8srQ5/UYWhI+jlm3Vo3D3QfXoRyQ9hOn3ynJGtwusKui4+uDi4gykdUFYCITZxK+C/fOCAZNJ+YaKSm/OaiXw== +on_finish: +- pwsh: | + Write-Host "Cleaning up NuGet cache..." + Remove-Item -Recurse -Force "$env:LOCALAPPDATA\NuGet\v3-cache" + Write-Host "NuGet cache cleaned." + + Write-Host "Cleaning up .NET install folder..." + Remove-Item -Recurse -Force "C:\Users\Public\Documents\dotnet_install" + Write-Host ".NET install folder cleaned." diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs index 54ebed7ac3..55b4c8ab17 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; @@ -110,6 +111,13 @@ public void NewEmptyObjectSerializeAsEmptyObject() Assert.AreEqual(result, "{}"); } + [TestMethod] + public void StructCanDeserialize() + { + SampleStruct sample = JsonSerializer.Deserialize(SampleObjectJson); + Assert.AreEqual(sample.A, 1); + } + private sealed class SampleDelegatePropertyClass { public int A { get => B; set => B = value; } @@ -161,4 +169,10 @@ string ToJson() return JsonSerializer.Serialize(this); } } + + private struct SampleStruct + { + [JsonInclude] + public int A; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs index 449b6c2edd..eb5a1b07e5 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GameRegistryContentTest.cs @@ -40,7 +40,8 @@ private static void GetRegistryContentCore(string subkey) { RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!, RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!), - _ => throw new NotImplementedException() + RegistryValueKind.String => (string)gameKey.GetValue(valueName)!, + _ => throw new ArgumentException($"Unsupported type: {gameKey.GetValueKind(valueName)}"), }; } diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs index 1f7c08a6b1..4e8cc9f465 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/UnlockerIslandFunctionOffsetTest.cs @@ -14,23 +14,45 @@ public class UnlockerIslandFunctionOffsetTest [TestMethod] public void GenerateJson() { + // FunctionOffsetMickeyWonderMethod: + // public static byte[] AnonymousMethod43(int nType) -> jmp xxxxxxxx -> another xref to xxxxxxxx + // FunctionOffsetMickeyWonderMethodPartner: + // public static string get_unityVersion() -> jmp + // FunctionOffsetMickeyWonderMethodPartner2: + // "4C 8B 05 ?? ?? ?? ?? 4C 89 F1 48 89 FA E8 ?? ?? ?? ?? 90 48 83 C4 28 5B 5F 5E 41 5E C3" + // FunctionOffsetSetFieldOfView: // public void set_fieldOfView(float value) -> jmp xxxxxxxx + // FunctionOffsetSetTargetFrameRate: // public static void set_targetFrameRate(int value) -> jmp xxxxxxxxx (to the end) + // FunctionOffsetSetEnableFogRendering: // public static void set_enableFogRendering(bool value) -> jmp xxxxxxxxx (to the end) - + // FunctionOffsetOpenTeam: + // public static void AJODMEAHOGI() + // FunctionOffsetOpenTeamPageAccordingly: + // public static void OEEFGJDOCJJ(bool KCBOKOCOGEI = true) UnlockerIslandConfigurationWrapper wrapper = new() { Chinese = new() { + FunctionOffsetMickeyWonderMethod = 0x099D1D80, + FunctionOffsetMickeyWonderMethodPartner = 0x0092D5F0, + FunctionOffsetMickeyWonderMethodPartner2 = 0x054AEF80, FunctionOffsetSetFieldOfView = 0x01136D30, FunctionOffsetSetTargetFrameRate = 0x0131E600, FunctionOffsetSetEnableFogRendering = 0x015DC790, + FunctionOffsetOpenTeam = 0x07806530, + FunctionOffsetOpenTeamPageAccordingly = 0x0781D3F0, }, Oversea = new() { + FunctionOffsetMickeyWonderMethod = 0x09B37E60, + FunctionOffsetMickeyWonderMethodPartner = 0x0092D7D0, + FunctionOffsetMickeyWonderMethodPartner2 = 0x054AEE50, FunctionOffsetSetFieldOfView = 0x01136F30, FunctionOffsetSetTargetFrameRate = 0x0131E800, FunctionOffsetSetEnableFogRendering = 0x015DC990, + FunctionOffsetOpenTeam = 0x0777E4F0, + FunctionOffsetOpenTeamPageAccordingly = 0x07779870, }, }; @@ -39,17 +61,27 @@ public void GenerateJson() private sealed class UnlockerIslandConfigurationWrapper { - public required UnlockerIslandConfiguration Oversea { get; set; } - public required UnlockerIslandConfiguration Chinese { get; set; } + + public required UnlockerIslandConfiguration Oversea { get; set; } } private sealed class UnlockerIslandConfiguration { + public required uint FunctionOffsetMickeyWonderMethod { get; set; } + + public required uint FunctionOffsetMickeyWonderMethodPartner { get; set; } + + public required uint FunctionOffsetMickeyWonderMethodPartner2 { get; set; } + public required uint FunctionOffsetSetFieldOfView { get; set; } public required uint FunctionOffsetSetTargetFrameRate { get; set; } public required uint FunctionOffsetSetEnableFogRendering { get; set; } + + public required uint FunctionOffsetOpenTeam { get; set; } + + public required uint FunctionOffsetOpenTeamPageAccordingly { get; set; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj index 8ec9d4f9ad..4d0aef284c 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj +++ b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj @@ -14,8 +14,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Snap.Hutao/Snap.Hutao/BannedSymbols.txt b/src/Snap.Hutao/Snap.Hutao/BannedSymbols.txt new file mode 100644 index 0000000000..39de39a193 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/BannedSymbols.txt @@ -0,0 +1,2 @@ +// https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md +M:System.Collections.Generic.List`1.ForEach(System.Action{`0}) \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs index db9b5b6473..9ee82397f8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -77,7 +77,7 @@ public async ValueTask GetFileFromCacheAsync(Uri uri, ElementTheme th // If the file already exists, we don't need to download it again if (!IsFileInvalid(defaultFilePath)) { - await ConvertToMonoChromeAndSaveFileAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false); + await TryConvertToMonoChromeAndSaveFileAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false); return themeOrDefaultFilePath; } @@ -86,7 +86,7 @@ public async ValueTask GetFileFromCacheAsync(Uri uri, ElementTheme th // File may be downloaded by another thread if (!IsFileInvalid(defaultFilePath)) { - await ConvertToMonoChromeAndSaveFileAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false); + await TryConvertToMonoChromeAndSaveFileAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false); return themeOrDefaultFilePath; } @@ -122,9 +122,9 @@ private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = tru return new FileInfo(file).Length == 0; } - private static async ValueTask ConvertToMonoChromeAndSaveFileAsync(string sourceFile, string themeFile, ElementTheme theme) + private static async ValueTask TryConvertToMonoChromeAndSaveFileAsync(string sourceFile, string themeFile, ElementTheme theme) { - if (string.Equals(sourceFile, themeFile, StringComparison.OrdinalIgnoreCase)) + if (theme is ElementTheme.Default || string.Equals(sourceFile, themeFile, StringComparison.OrdinalIgnoreCase)) { return; } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorker.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorker.cs index 98961be661..87335b64ee 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Sharding/HttpShardCopyWorker.cs @@ -60,18 +60,18 @@ private async ValueTask CopyShardAsync(IHttpShard shard, IProgress memoryOwner = MemoryPool.Shared.Rent(options.BufferSize)) { Memory buffer = memoryOwner.Memory; - using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false)) + using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(true)) { int bytesReadSinceLastReport = 0; do { - using (await shard.ReaderWriterLock.ReaderLockAsync().ConfigureAwait(false)) + using (await shard.ReaderWriterLock.ReaderLockAsync().ConfigureAwait(true)) { if (shard.BytesRead >= shard.End - shard.Start) { @@ -81,13 +81,13 @@ private async ValueTask CopyShardAsync(IHttpShard shard, IProgress buffer = stackalloc byte[sizeof(VOLUME_DISK_EXTENTS) + (sizeof(DISK_EXTENT) * 1)]; + if (DeviceIoControl(hLogicalDriver, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, default, default, buffer, default, default)) + { + deviceNumber = MemoryMarshal.AsRef(buffer).Extents[0].DiskNumber; + return; + } + + WIN32_ERROR error2 = GetLastError(); + if (error2 is not WIN32_ERROR.ERROR_MORE_DATA) + { + Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error2)); + } + + // The volume has multiple extents. + buffer = stackalloc byte[sizeof(VOLUME_DISK_EXTENTS) + (sizeof(DISK_EXTENT) * (int)MemoryMarshal.AsRef(buffer).NumberOfDiskExtents)]; + if (DeviceIoControl(hLogicalDriver, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, default, default, buffer, default, default)) + { + deviceNumber = MemoryMarshal.AsRef(buffer).Extents[0].DiskNumber; + return; } - deviceNumber = number.DeviceNumber; + Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(GetLastError())); + throw HutaoException.Throw("Failed to get the device number."); } finally { diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFileStream.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFileStream.cs index bb03d752a5..e786b3ddfc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFileStream.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/TempFileStream.cs @@ -11,8 +11,13 @@ internal sealed partial class TempFileStream : Stream private readonly FileStream stream; public TempFileStream(FileMode mode, FileAccess access) + : this(Path.GetTempFileName(), mode, access) { - path = Path.GetTempFileName(); + } + + private TempFileStream(string path, FileMode mode, FileAccess access) + { + this.path = path; stream = File.Open(path, mode, access); } @@ -26,6 +31,13 @@ public TempFileStream(FileMode mode, FileAccess access) public override long Position { get => stream.Position; set => stream.Position = value; } + public static TempFileStream CopyFrom(string file, FileMode mode, FileAccess access) + { + string path = Path.GetTempFileName(); + File.Copy(file, path, true); + return new TempFileStream(path, mode, access); + } + public override void Flush() { stream.Flush(); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs index cfe1cb1574..0ef543e7a6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs @@ -100,10 +100,7 @@ async ValueTask ActivateAndInitializeAsync() public async ValueTask HandleLaunchGameActionAsync(string? uid = null) { - serviceProvider - .GetRequiredService() - .Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid); - + // TODO: Handle uid selection, notify user service to switch to the corresponding user await taskContext.SwitchToMainThreadAsync(); switch (currentWindowReference.Window) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs index 2e01bd2ce5..99245c22c9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs @@ -3,40 +3,39 @@ namespace Snap.Hutao.Core; -[Obsolete("This class only exist for binding purpose")] [Injection(InjectAs.Singleton)] internal sealed class RuntimeOptions { - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public Version Version { get => HutaoRuntime.Version; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public string UserAgent { get => HutaoRuntime.UserAgent; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public string DataFolder { get => HutaoRuntime.DataFolder; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public string LocalCache { get => HutaoRuntime.LocalCache; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public string FamilyName { get => HutaoRuntime.FamilyName; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public string DeviceId { get => HutaoRuntime.DeviceId; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public string WebView2Version { get => HutaoRuntime.WebView2Version.Version; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public bool IsWebView2Supported { get => HutaoRuntime.WebView2Version.Supported; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public bool IsElevated { get => HutaoRuntime.IsProcessElevated; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public bool IsToastAvailable { get => HutaoRuntime.IsAppNotificationEnabled; } - [Obsolete] + [Obsolete("This property only exist for binding purpose")] public DateTimeOffset AppLaunchTime { get => HutaoRuntime.LaunchTime; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Scripting/SnapHutaoDiagnostics.cs b/src/Snap.Hutao/Snap.Hutao/Core/Scripting/HutaoDiagnostics.cs similarity index 100% rename from src/Snap.Hutao/Snap.Hutao/Core/Scripting/SnapHutaoDiagnostics.cs rename to src/Snap.Hutao/Snap.Hutao/Core/Scripting/HutaoDiagnostics.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs index e32c3dec58..cb823cf7c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs @@ -11,7 +11,9 @@ internal static class SettingKeys { #region MainWindow public const string WindowRect = "WindowRect"; + public const string WindowScale = "WindowScale"; public const string GuideWindowRect = "GuideWindowRect"; + public const string GuideWindowScale = "GuideWindowScale"; public const string IsNavPaneOpen = "IsNavPaneOpen"; public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked"; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs index a6d2bee33a..be41aed024 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs @@ -7,6 +7,15 @@ namespace Snap.Hutao.Core.Threading; internal static class SpinWaitPolyfill { + public static unsafe void SpinWhile(delegate* condition) + { + SpinWait spinner = default; + while (condition()) + { + spinner.SpinOnce(); + } + } + public static unsafe void SpinUntil(ref readonly T state, delegate* condition) { SpinWait spinner = default; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs index 656de2d290..fab1dbcd52 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs @@ -2,24 +2,34 @@ // Licensed under the MIT license. using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; namespace Snap.Hutao.Core.Threading; [Injection(InjectAs.Singleton, typeof(ITaskContext))] internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe { - private readonly DispatcherQueueSynchronizationContext synchronizationContext; private readonly DispatcherQueue dispatcherQueue; public TaskContext() { dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - synchronizationContext = new(dispatcherQueue); + DispatcherQueueSynchronizationContext synchronizationContext = new(dispatcherQueue); SynchronizationContext.SetSynchronizationContext(synchronizationContext); } public DispatcherQueue DispatcherQueue { get => dispatcherQueue; } + public static ITaskContext GetForDependencyObject(DependencyObject dependencyObject) + { + return GetForDispatcherQueue(dependencyObject.DispatcherQueue); + } + + public static ITaskContext GetForDispatcherQueue(DispatcherQueue dispatcherQueue) + { + return new TaskContextWrapperForDispatcherQueue(dispatcherQueue); + } + public ThreadPoolSwitchOperation SwitchToBackgroundAsync() { return new(dispatcherQueue); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContextWrapperForDispatcherQueue.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContextWrapperForDispatcherQueue.cs new file mode 100644 index 0000000000..3036d30775 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContextWrapperForDispatcherQueue.cs @@ -0,0 +1,41 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Dispatching; + +namespace Snap.Hutao.Core.Threading; + +internal sealed class TaskContextWrapperForDispatcherQueue : ITaskContext +{ + private readonly DispatcherQueue dispatcherQueue; + + public TaskContextWrapperForDispatcherQueue(DispatcherQueue dispatcherQueue) + { + this.dispatcherQueue = dispatcherQueue; + } + + public void BeginInvokeOnMainThread(Action action) + { + dispatcherQueue.TryEnqueue(() => action()); + } + + public void InvokeOnMainThread(Action action) + { + dispatcherQueue.Invoke(action); + } + + public T InvokeOnMainThread(Func action) + { + return dispatcherQueue.Invoke(action); + } + + public ThreadPoolSwitchOperation SwitchToBackgroundAsync() + { + return new(dispatcherQueue); + } + + public DispatcherQueueSwitchOperation SwitchToMainThreadAsync() + { + return new(dispatcherQueue); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs index 05ff1d2318..7d4ee53a73 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs @@ -38,7 +38,7 @@ public async ValueTask CreateForConfirmAsync(string title, RequestedTheme = appOptions.ElementTheme, }; - return await EnqueueAndShowAsync(dialog).ConfigureAwait(false); + return await EnqueueAndShowAsync(dialog).ShowTask.ConfigureAwait(false); } public async ValueTask CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close) @@ -56,7 +56,7 @@ public async ValueTask CreateForConfirmCancelAsync(string t RequestedTheme = appOptions.ElementTheme, }; - return await EnqueueAndShowAsync(dialog).ConfigureAwait(false); + return await EnqueueAndShowAsync(dialog).ShowTask.ConfigureAwait(false); } public async ValueTask CreateForIndeterminateProgressAsync(string title) @@ -96,9 +96,8 @@ public TContentDialog CreateInstance(params object[] parameters) return contentDialog; } - [SuppressMessage("", "SH003")] - public Task EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog, TaskCompletionSource? dialogShowSource = default) + public ValueContentDialogTask EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog) { - return contentDialogQueue.EnqueueAndShowAsync(contentDialog, dialogShowSource); + return contentDialogQueue.EnqueueAndShowAsync(contentDialog); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactoryExtension.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactoryExtension.cs new file mode 100644 index 0000000000..9c09335876 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactoryExtension.cs @@ -0,0 +1,18 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; + +namespace Snap.Hutao.Factory.ContentDialog; + +internal static class ContentDialogFactoryExtension +{ + public static async ValueTask BlockAsync(this IContentDialogFactory contentDialogFactory, Microsoft.UI.Xaml.Controls.ContentDialog contentDialog) + { + TaskCompletionSource dialogShowSource = new(); + ValueContentDialogTask dialogTask = contentDialogFactory.EnqueueAndShowAsync(contentDialog); + await dialogTask.QueueTask.ConfigureAwait(false); + contentDialog.DispatcherQueue.TryEnqueue(() => contentDialog.Focus(FocusState.Programmatic)); + return new(contentDialog); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogQueue.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogQueue.cs index fdefe3f476..81565c22cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogQueue.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogQueue.cs @@ -25,24 +25,25 @@ internal sealed partial class ContentDialogQueue : IContentDialogQueue public bool IsDialogShowing { get => currentWindowReference.Window is not null && isDialogShowing; } [SuppressMessage("", "SH100")] - public Task EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog, TaskCompletionSource? dialogShowSource = default) + public ValueContentDialogTask EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog) { - TaskCompletionSource dialogResultSource = new(); + TaskCompletionSource queueSource = new(); + TaskCompletionSource resultSource = new(); dialogQueue.Enqueue(async () => { try { await taskContext.SwitchToMainThreadAsync(); - dialogShowSource?.TrySetResult(); + queueSource.TrySetResult(); contentDialog.ShowAsync().AsTask() - .ContinueWith(Continuation, dialogResultSource, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) + .ContinueWith(Continuation, resultSource, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) .SafeForget(logger); contentDialog.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); } catch (Exception ex) { - dialogResultSource.SetException(ex); + resultSource.SetException(ex); } finally { @@ -55,7 +56,7 @@ public Task EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls. ShowNextDialog(); } - return dialogResultSource.Task; + return new(queueSource.Task, resultSource.Task); } private static void RunContinuation(Task task, object? state) diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogScope.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogScope.cs new file mode 100644 index 0000000000..241a50554e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogScope.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.UI.Xaml; + +namespace Snap.Hutao.Factory.ContentDialog; + +internal struct ContentDialogScope : IDisposable +{ + private readonly Microsoft.UI.Xaml.Controls.ContentDialog contentDialog; + + private bool disposing = false; + private bool disposed = false; + + public ContentDialogScope(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog) + { + this.contentDialog = contentDialog; + } + + public void Dispose() + { + if (!disposed && !disposing) + { + disposing = true; + contentDialog.DispatcherQueue.Invoke(contentDialog.Hide); + disposing = false; + disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs index eed622fea5..ba0eadaf5a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs @@ -5,38 +5,16 @@ namespace Snap.Hutao.Factory.ContentDialog; -/// -/// 内容对话框工厂 -/// -[HighQuality] internal interface IContentDialogFactory { bool IsDialogShowing { get; } ITaskContext TaskContext { get; } - /// - /// 异步确认 - /// - /// 标题 - /// 内容 - /// 结果 ValueTask CreateForConfirmAsync(string title, string content); - /// - /// 异步确认或取消 - /// - /// 标题 - /// 内容 - /// 默认按钮 - /// 结果 ValueTask CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close); - /// - /// 异步创建一个新的内容对话框,用于提示未知的进度 - /// - /// 标题 - /// 内容对话框 ValueTask CreateForIndeterminateProgressAsync(string title); TContentDialog CreateInstance(params object[] parameters) @@ -46,5 +24,5 @@ ValueTask CreateInstanceAsync(params object[] pa where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog; [SuppressMessage("", "SH003")] - Task EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog, TaskCompletionSource? dialogShowSource = default); + ValueContentDialogTask EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogQueue.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogQueue.cs index 268eccf958..4fab2b11f0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogQueue.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogQueue.cs @@ -10,5 +10,5 @@ internal interface IContentDialogQueue { bool IsDialogShowing { get; } - Task EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog, TaskCompletionSource? dialogShowSource = null); + ValueContentDialogTask EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ValueContentDialogTask.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ValueContentDialogTask.cs new file mode 100644 index 0000000000..5c484534ef --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ValueContentDialogTask.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; + +namespace Snap.Hutao.Factory.ContentDialog; + +internal readonly struct ValueContentDialogTask +{ + /// + /// This task will be completed when the associated dialog finishes queueing and starts to show. + /// + public readonly Task QueueTask; + + /// + /// This task will be completed when the associated dialog closed in any reason. + /// + public readonly Task ShowTask; + + public ValueContentDialogTask(Task queueTask, Task showTask) + { + QueueTask = queueTask; + ShowTask = showTask; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs index d22f5a05b8..871a32b847 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs @@ -55,7 +55,6 @@ public uint LevelCurrent set => SetProperty(LevelCurrent, value, v => LocalSetting.Set(SettingKeys.CultivationAvatarLevelCurrent, v)); } - /// public uint LevelTarget { get => LocalSetting.Get(SettingKeys.CultivationAvatarLevelTarget, LevelMax); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs index 14d93322ed..e42ebab84a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs @@ -9,8 +9,8 @@ internal interface ICalculableSource public TResult ToCalculable(); } -internal interface ITypedCalculableSource +internal interface ITypedCalculableSource where TResult : ICalculable { - public TResult ToCalculable(TIndex param); + public TResult ToCalculable(TType param); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs index ff12a5db24..75248f6746 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs @@ -11,36 +11,18 @@ namespace Snap.Hutao.Model.Entity; -/// -/// 游戏内账号 -/// -[HighQuality] [Table("game_accounts")] internal sealed partial class GameAccount : ObservableObject, IAppDbEntity, IReorderable, IAdvancedCollectionViewItem { - /// - /// 内部Id - /// [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid InnerId { get; set; } - /// - /// 对应的Uid - /// - public string? AttachUid { get; set; } - - /// - /// 服务器类型 - /// public SchemeType Type { get; set; } - /// - /// 名称 - /// public string Name { get; set; } = default!; /// @@ -61,20 +43,6 @@ public static GameAccount From(string name, string sdk, SchemeType type) }; } - /// - /// 更新绑定的Uid - /// - /// uid - public void UpdateAttachUid(string? uid) - { - AttachUid = uid; - OnPropertyChanged(nameof(AttachUid)); - } - - /// - /// 更新名称 - /// - /// 新名称 public void UpdateName(string name) { Name = name; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs index 14ea795b7c..7959196087 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.Graphics.Canvas.Brushes; + namespace Snap.Hutao.Model.Entity; /// @@ -26,6 +28,7 @@ internal sealed partial class SettingEntry public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl"; public const string DownloadSpeedLimitPerSecondInKiloByte = "DownloadSpeedLimitPerSecondInKiloByte"; + public const string PackageConverterType = "PackageConverterType"; public const string BridgeShareSaveType = "BridgeShareSaveType"; @@ -35,9 +38,7 @@ internal sealed partial class SettingEntry public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame"; public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl"; - public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled"; - - public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled"; + public const string LaunchAreCommandLineArgumentsEnabled = "Launch.IsLaunchOptionsEnabled"; public const string LaunchIsExclusive = "Launch.IsExclusive"; public const string LaunchIsFullScreen = "Launch.IsFullScreen"; public const string LaunchIsBorderless = "Launch.IsBorderless"; @@ -45,11 +46,17 @@ internal sealed partial class SettingEntry public const string LaunchIsScreenWidthEnabled = "Launch.IsScreenWidthEnabled"; public const string LaunchScreenHeight = "Launch.ScreenHeight"; public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled"; - public const string LaunchUnlockFps = "Launch.UnlockFps"; - public const string LaunchTargetFps = "Launch.TargetFps"; + public const string LaunchIsIslandEnabled = "Launch.UnlockFps"; + public const string LaunchHookingSetFieldOfView = "Launch.HookingSetFieldOfView"; + public const string LaunchIsSetFieldOfViewEnabled = "Launch.IsSetFieldOfViewEnabled"; public const string LaunchTargetFov = "Launch.TargetFov"; + public const string LaunchFixLowFovScene = "Launch.FixLowFovScene"; public const string LaunchDisableFog = "Launch.DisableFog"; - public const string LaunchLoopAdjustFpsOnly = "Launch.LoopAdjustFpsOnly"; + public const string LaunchIsSetTargetFrameRateEnabled = "Launch.IsSetTargetFrameRateEnabled"; + public const string LaunchTargetFps = "Launch.TargetFps"; + public const string LaunchHookingOpenTeam = "Launch.HookingOpenTeam"; + public const string LaunchRemoveOpenTeamProgress = "Launch.RemoveOpenTeamProgress"; + public const string LaunchHookingMickyWonderPartner2 = "Launch.HookingMickyWonderPartner2"; public const string LaunchMonitor = "Launch.Monitor"; public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled"; public const string LaunchUsingCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile"; @@ -67,4 +74,10 @@ internal sealed partial class SettingEntry [Obsolete("不再使用 PowerShell")] public const string PowerShellPath = "PowerShellPath"; + + [Obsolete("不再要求智力测试")] + public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled"; + + [Obsolete("不再使用此名称")] + public const string LaunchLoopAdjustFpsOnly = "Launch.LoopAdjustFpsOnly"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs b/src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs index a6a49ff9b4..55af46d4f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/INameIcon.cs @@ -3,19 +3,9 @@ namespace Snap.Hutao.Model; -/// -/// 名称与图标 -/// -[HighQuality] internal interface INameIcon { - /// - /// 名称 - /// string Name { get; } - /// - /// 图标 - /// Uri Icon { get; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Item.cs b/src/Snap.Hutao/Snap.Hutao/Model/Item.cs index e82ea419ee..1e67dc83ef 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Item.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Item.cs @@ -5,29 +5,15 @@ namespace Snap.Hutao.Model; -/// -/// 物品基类 -/// -[HighQuality] internal class Item : INameIcon { - /// - /// 物品名称 - /// - public string Name { get; set; } = default!; + public string Name { get; init; } = default!; - /// - /// 主图标 - /// - public Uri Icon { get; set; } = default!; + public Uri Icon { get; init; } = default!; - /// - /// 小图标 - /// - public Uri Badge { get; set; } = default!; + public Uri Badge { get; init; } = default!; - /// - /// 星级 - /// - public QualityType Quality { get; set; } + public QualityType Quality { get; init; } + + internal uint Id { get; init; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ICultivationItemsAccess.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ICultivationItemsAccess.cs index dd1fab0b25..51ed82a1d2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ICultivationItemsAccess.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ICultivationItemsAccess.cs @@ -5,9 +5,7 @@ namespace Snap.Hutao.Model.Metadata.Abstraction; -internal interface ICultivationItemsAccess +internal interface ICultivationItemsAccess : INameAccess { - string Name { get; } - List CultivationItems { get; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IItemConvertible.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IItemConvertible.cs index 535d7b7e59..8e1c4d9fcf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IItemConvertible.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IItemConvertible.cs @@ -5,5 +5,6 @@ namespace Snap.Hutao.Model.Metadata.Abstraction; internal interface IItemConvertible { - Model.Item ToItem(); + TItem ToItem() + where TItem : Model.Item, new(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameAccess.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameAccess.cs new file mode 100644 index 0000000000..1bd57521b5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameAccess.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Metadata.Abstraction; + +internal interface INameAccess +{ + string Name { get; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameQualityAccess.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameQualityAccess.cs index a6d83c8a24..bb516c98b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameQualityAccess.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/INameQualityAccess.cs @@ -5,19 +5,7 @@ namespace Snap.Hutao.Model.Metadata.Abstraction; -/// -/// 物品与星级 -/// -[HighQuality] -internal interface INameQualityAccess +internal interface INameQualityAccess : INameAccess { - /// - /// 名称 - /// - string Name { get; } - - /// - /// 星级 - /// QualityType Quality { get; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemConvertible.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemConvertible.cs index fb51bec996..f98f017d2a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemConvertible.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/IStatisticsItemConvertible.cs @@ -5,16 +5,7 @@ namespace Snap.Hutao.Model.Metadata.Abstraction; -/// -/// 指示该类为统计物品的源 -/// -[HighQuality] internal interface IStatisticsItemConvertible { - /// - /// 转换到统计物品 - /// - /// 个数 - /// 统计物品 StatisticsItem ToStatisticsItem(int count); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemConvertible.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemConvertible.cs index 976473100f..1732f016df 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemConvertible.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemConvertible.cs @@ -6,10 +6,6 @@ namespace Snap.Hutao.Model.Metadata.Abstraction; -/// -/// 指示该类为简述统计物品的源 -/// -[HighQuality] internal interface ISummaryItemConvertible { QualityType Quality { get; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs index e0e11d9c82..1c92b1fcac 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Achievement.cs @@ -5,59 +5,29 @@ namespace Snap.Hutao.Model.Metadata.Achievement; -/// -/// 成就 -/// -[HighQuality] internal sealed class Achievement { - /// - /// Id - /// public AchievementId Id { get; set; } - /// - /// 分类Id - /// public AchievementGoalId Goal { get; set; } - /// - /// 排序顺序 - /// public uint Order { get; set; } - /// - /// 标题 - /// + public AchievementId PreviousId { get; set; } + public string Title { get; set; } = default!; - /// - /// 描述 - /// public string Description { get; set; } = default!; - /// - /// 完成奖励 - /// public Reward FinishReward { get; set; } = default!; - /// - /// 总进度 - /// + public bool IsDeleteWatcherAfterFinish { get; set; } + public uint Progress { get; set; } - /// - /// 图标 - /// public string? Icon { get; set; } - /// - /// 版本 - /// public string Version { get; set; } = default!; - /// - /// 是否为委托成就 - /// public bool IsDailyQuest { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs index 7c3695e4bf..554ef08b36 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/AchievementGoal.cs @@ -5,34 +5,15 @@ namespace Snap.Hutao.Model.Metadata.Achievement; -/// -/// 成就分类 -/// -[HighQuality] internal sealed class AchievementGoal { - /// - /// Id - /// public AchievementGoalId Id { get; set; } - /// - /// 排序顺序 - /// public uint Order { get; set; } - /// - /// 名称 - /// public string Name { get; set; } = default!; - /// - /// 完成奖励 - /// public Reward? FinishReward { get; set; } - /// - /// 图标 - /// public string Icon { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs index 69452ed303..3983771c0a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Achievement/Reward.cs @@ -3,7 +3,4 @@ namespace Snap.Hutao.Model.Metadata.Achievement; -/// -/// 奖励 -/// internal sealed class Reward : IdCount; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs index e085e3fc04..7a839c5b56 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs @@ -15,7 +15,6 @@ namespace Snap.Hutao.Model.Metadata.Avatar; -[HighQuality] [DebuggerDisplay("Name={Name},Id={Id}")] internal partial class Avatar : INameQualityAccess, IStatisticsItemConvertible, @@ -59,6 +58,8 @@ internal partial class Avatar : INameQualityAccess, public List CultivationItems { get; set; } = default!; + public NameCard NameCard { get; set; } = default!; + [JsonIgnore] public AvatarCollocationView? CollocationView { get; set; } @@ -81,10 +82,12 @@ public ICalculableAvatar ToCalculable() return CalculableAvatar.From(this); } - public Model.Item ToItem() + public TItem ToItem() + where TItem : Model.Item, new() { return new() { + Id = Id, Name = Name, Icon = AvatarIconConverter.IconNameToUri(Icon), Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore), @@ -96,6 +99,7 @@ public StatisticsItem ToStatisticsItem(int count) { return new() { + Id = Id, Name = Name, Icon = AvatarIconConverter.IconNameToUri(Icon), Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore), @@ -109,6 +113,7 @@ public SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp { return new() { + Id = Id, Name = Name, Icon = AvatarIconConverter.IconNameToUri(Icon), Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore), diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/NameCard.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/NameCard.cs new file mode 100644 index 0000000000..f589c23a2e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/NameCard.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Metadata.Avatar; + +internal sealed class NameCard : NameDescription +{ + public string Icon { get; set; } = default!; + + public string PicturePrefix { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardIconConverter.cs new file mode 100644 index 0000000000..3f2f4e33f9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardIconConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.UI.Xaml.Data.Converter; +using Snap.Hutao.Web.Endpoint.Hutao; + +namespace Snap.Hutao.Model.Metadata.Converter; + +internal sealed partial class AvatarNameCardIconConverter : ValueConverter, IIconNameToUriConverter +{ + public static Uri IconNameToUri(string name) + { + if (string.IsNullOrEmpty(name)) + { + return default!; + } + + return StaticResourcesEndpoints.StaticRaw("NameCardIcon", $"{name}.png").ToUri(); + } + + public override Uri Convert(string from) + { + return IconNameToUri(from); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicAlphaConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicAlphaConverter.cs new file mode 100644 index 0000000000..dc1dcf7774 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicAlphaConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.UI.Xaml.Data.Converter; +using Snap.Hutao.Web.Endpoint.Hutao; + +namespace Snap.Hutao.Model.Metadata.Converter; + +internal sealed partial class AvatarNameCardPicAlphaConverter : ValueConverter, IIconNameToUriConverter +{ + public static Uri IconNameToUri(string name) + { + if (string.IsNullOrEmpty(name)) + { + return default!; + } + + return StaticResourcesEndpoints.StaticRaw("NameCardPicAlpha", $"{name}_Alpha.png").ToUri(); + } + + public override Uri Convert(string from) + { + return IconNameToUri(from); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs index 9e61515efe..3602385a3e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs @@ -6,31 +6,20 @@ namespace Snap.Hutao.Model.Metadata.Converter; -internal sealed partial class AvatarNameCardPicConverter : ValueConverter +internal sealed partial class AvatarNameCardPicConverter : ValueConverter, IIconNameToUriConverter { - public static Uri AvatarToUri(Avatar.Avatar? avatar) + public static Uri IconNameToUri(string name) { - if (avatar is null) + if (string.IsNullOrEmpty(name)) { return default!; } - string avatarName = ReplaceSpecialCaseNaming(avatar.Icon["UI_AvatarIcon_".Length..]); - return StaticResourcesEndpoints.StaticRaw("NameCardPic", $"UI_NameCardPic_{avatarName}_P.png").ToUri(); + return StaticResourcesEndpoints.StaticRaw("NameCardPic", $"{name}_P.png").ToUri(); } - public override Uri Convert(Avatar.Avatar? avatar) + public override Uri Convert(string from) { - return AvatarToUri(avatar); - } - - private static string ReplaceSpecialCaseNaming(string avatarName) - { - return avatarName switch - { - "Yae" => "Yae1", - "Momoka" => "Kirara", - _ => avatarName, - }; + return IconNameToUri(from); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs index 8b21fb6cf1..20d00c0464 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs @@ -4,196 +4,63 @@ using Snap.Hutao.Model.Primitive; using Snap.Hutao.ViewModel.Cultivation; using System.Collections.Frozen; +using System.Collections.Immutable; namespace Snap.Hutao.Model.Metadata.Item; internal static class MaterialIds { - public static FrozenSet MondayThursdayItems { get; } = FrozenSet.ToFrozenSet( + private static readonly ImmutableArray Entries = [ - 104301U, - 104302U, - 104303U, // 「自由」 - 104310U, - 104311U, - 104312U, // 「繁荣」 - 104320U, - 104321U, - 104322U, // 「浮世」 - 104329U, - 104330U, - 104331U, // 「诤言」 - 104338U, - 104339U, - 104340U, // 「公平」 - 104347U, - 104348U, - 104349U, // 「角逐」 - 114001U, - 114002U, - 114003U, - 114004U, // 高塔孤王 - 114013U, - 114014U, - 114015U, - 114016U, // 孤云寒林 - 114025U, - 114026U, - 114027U, - 114028U, // 远海夷地 - 114037U, - 114038U, - 114039U, - 114040U, // 谧林涓露 - 114049U, - 114050U, - 114051U, - 114052U, // 悠古弦音 - 114061U, - 114062U, - 114063U, - 114064U, // 贡祭炽心 - ]); + new(DaysOfWeek.MondayAndThursday, 104301U, 104302U, 104303U), // 「自由」 + new(DaysOfWeek.MondayAndThursday, 104310U, 104311U, 104312U), // 「繁荣」 + new(DaysOfWeek.MondayAndThursday, 104320U, 104321U, 104322U), // 「浮世」 + new(DaysOfWeek.MondayAndThursday, 104329U, 104330U, 104331U), // 「诤言」 + new(DaysOfWeek.MondayAndThursday, 104338U, 104339U, 104340U), // 「公平」 + new(DaysOfWeek.MondayAndThursday, 104347U, 104348U, 104349U), // 「角逐」 + new(DaysOfWeek.TuesdayAndFriday, 104304U, 104305U, 104306U), // 「抗争」 + new(DaysOfWeek.TuesdayAndFriday, 104313U, 104314U, 104315U), // 「勤劳」 + new(DaysOfWeek.TuesdayAndFriday, 104323U, 104324U, 104325U), // 「风雅」 + new(DaysOfWeek.TuesdayAndFriday, 104332U, 104333U, 104334U), // 「巧思」 + new(DaysOfWeek.TuesdayAndFriday, 104341U, 104342U, 104343U), // 「正义」 + new(DaysOfWeek.TuesdayAndFriday, 104350U, 104351U, 104352U), // 「焚燔」 + new(DaysOfWeek.WednesdayAndSaturday, 104307U, 104308U, 104309U), // 「诗文」 + new(DaysOfWeek.WednesdayAndSaturday, 104316U, 104317U, 104318U), // 「黄金」 + new(DaysOfWeek.WednesdayAndSaturday, 104326U, 104327U, 104328U), // 「天光」 + new(DaysOfWeek.WednesdayAndSaturday, 104335U, 104336U, 104337U), // 「笃行」 + new(DaysOfWeek.WednesdayAndSaturday, 104344U, 104345U, 104346U), // 「秩序」 + new(DaysOfWeek.WednesdayAndSaturday, 104353U, 104354U, 104355U), // 「纷争」 + new(DaysOfWeek.MondayAndThursday, 114001U, 114002U, 114003U, 114004U), // 高塔孤王 + new(DaysOfWeek.MondayAndThursday, 114013U, 114014U, 114015U, 114016U), // 孤云寒林 + new(DaysOfWeek.MondayAndThursday, 114025U, 114026U, 114027U, 114028U), // 远海夷地 + new(DaysOfWeek.MondayAndThursday, 114037U, 114038U, 114039U, 114040U), // 谧林涓露 + new(DaysOfWeek.MondayAndThursday, 114049U, 114050U, 114051U, 114052U), // 悠古弦音 + new(DaysOfWeek.MondayAndThursday, 114061U, 114062U, 114063U, 114064U), // 贡祭炽心 + new(DaysOfWeek.TuesdayAndFriday, 114005U, 114006U, 114007U, 114008U), // 凛风奔狼 + new(DaysOfWeek.TuesdayAndFriday, 114017U, 114018U, 114019U, 114020U), // 雾海云间 + new(DaysOfWeek.TuesdayAndFriday, 114029U, 114030U, 114031U, 114032U), // 鸣神御灵 + new(DaysOfWeek.TuesdayAndFriday, 114041U, 114042U, 114043U, 114044U), // 绿洲花园 + new(DaysOfWeek.TuesdayAndFriday, 114053U, 114054U, 114055U, 114056U), // 纯圣露滴 + new(DaysOfWeek.TuesdayAndFriday, 114065U, 114066U, 114067U, 114068U), // 谵妄圣主 + new(DaysOfWeek.WednesdayAndSaturday, 114009U, 114010U, 114011U, 114012U), // 狮牙斗士 + new(DaysOfWeek.WednesdayAndSaturday, 114021U, 114022U, 114023U, 114024U), // 漆黑陨铁 + new(DaysOfWeek.WednesdayAndSaturday, 114033U, 114034U, 114035U, 114036U), // 今昔剧画 + new(DaysOfWeek.WednesdayAndSaturday, 114045U, 114046U, 114047U, 114048U), // 谧林涓露 + new(DaysOfWeek.WednesdayAndSaturday, 114057U, 114058U, 114059U, 114060U), // 无垢之海 + new(DaysOfWeek.WednesdayAndSaturday, 114069U, 114070U, 114071U, 114072U), // 神合秘烟 + ]; - public static FrozenSet MondayThursdayHighestRankItems { get; } = FrozenSet.ToFrozenSet( - [ - 104303U, // 「自由」 - 104312U, // 「繁荣」 - 104322U, // 「浮世」 - 104331U, // 「诤言」 - 104340U, // 「公平」 - 104349U, // 「角逐」 - 114004U, // 高塔孤王 - 114016U, // 孤云寒林 - 114028U, // 远海夷地 - 114040U, // 谧林涓露 - 114052U, // 悠古弦音 - 114064U, // 贡祭炽心 - ]); + public static FrozenSet MondayThursdayItems { get; } = Entries.Where(entry => entry.DaysOfWeek is DaysOfWeek.MondayAndThursday).SelectMany(entry => entry.Enumerate()).ToFrozenSet(); - public static FrozenSet TuesdayFridayItems { get; } = FrozenSet.ToFrozenSet( - [ - 104304U, - 104305U, - 104306U, // 「抗争」 - 104313U, - 104314U, - 104315U, // 「勤劳」 - 104323U, - 104324U, - 104325U, // 「风雅」 - 104332U, - 104333U, - 104334U, // 「巧思」 - 104341U, - 104342U, - 104343U, // 「正义」 - 104350U, - 104351U, - 104352U, // 「焚燔」 - 114005U, - 114006U, - 114007U, - 114008U, // 凛风奔狼 - 114017U, - 114018U, - 114019U, - 114020U, // 雾海云间 - 114029U, - 114030U, - 114031U, - 114032U, // 鸣神御灵 - 114041U, - 114042U, - 114043U, - 114044U, // 绿洲花园 - 114053U, - 114054U, - 114055U, - 114056U, // 纯圣露滴 - 114065U, - 114066U, - 114067U, - 114068U, // 谵妄圣主 - ]); + public static FrozenSet MondayThursdayEntries { get; } = Entries.Where(entry => entry.DaysOfWeek is DaysOfWeek.MondayAndThursday).ToFrozenSet(); - public static FrozenSet TuesdayFridayHighestRankItems { get; } = FrozenSet.ToFrozenSet( - [ - 104306U, // 「抗争」 - 104315U, // 「勤劳」 - 104325U, // 「风雅」 - 104334U, // 「巧思」 - 104343U, // 「正义」 - 104352U, // 「焚燔」 - 114008U, // 凛风奔狼 - 114020U, // 雾海云间 - 114032U, // 鸣神御灵 - 114044U, // 绿洲花园 - 114056U, // 纯圣露滴 - 114068U, // 谵妄圣主 - ]); + public static FrozenSet TuesdayFridayItems { get; } = Entries.Where(entry => entry.DaysOfWeek is DaysOfWeek.TuesdayAndFriday).SelectMany(entry => entry.Enumerate()).ToFrozenSet(); - public static FrozenSet WednesdaySaturdayItems { get; } = FrozenSet.ToFrozenSet( - [ - 104307U, - 104308U, - 104309U, // 「诗文」 - 104316U, - 104317U, - 104318U, // 「黄金」 - 104326U, - 104327U, - 104328U, // 「天光」 - 104335U, - 104336U, - 104337U, // 「笃行」 - 104344U, - 104345U, - 104346U, // 「秩序」 - 104353U, - 104354U, - 104355U, // 「纷争」 - 114009U, - 114010U, - 114011U, - 114012U, // 狮牙斗士 - 114021U, - 114022U, - 114023U, - 114024U, // 漆黑陨铁 - 114033U, - 114034U, - 114035U, - 114036U, // 今昔剧画 - 114045U, - 114046U, - 114047U, - 114048U, // 谧林涓露 - 114057U, - 114058U, - 114059U, - 114060U, // 无垢之海 - 114069U, - 114070U, - 114071U, - 114072U, // 神合秘烟 - ]); + public static FrozenSet TuesdayFridayEntries { get; } = Entries.Where(entry => entry.DaysOfWeek is DaysOfWeek.TuesdayAndFriday).ToFrozenSet(); - public static FrozenSet WednesdaySaturdayHighestRankItems { get; } = FrozenSet.ToFrozenSet( - [ - 104309U, // 「诗文」 - 104318U, // 「黄金」 - 104328U, // 「天光」 - 104337U, // 「笃行」 - 104346U, // 「秩序」 - 104355U, // 「纷争」 - 114012U, // 狮牙斗士 - 114024U, // 漆黑陨铁 - 114036U, // 今昔剧画 - 114048U, // 谧林涓露 - 114060U, // 无垢之海 - 114072U, // 神合秘烟 - ]); + public static FrozenSet WednesdaySaturdayItems { get; } = Entries.Where(entry => entry.DaysOfWeek is DaysOfWeek.WednesdayAndSaturday).SelectMany(entry => entry.Enumerate()).ToFrozenSet(); + + public static FrozenSet WednesdaySaturdayEntries { get; } = Entries.Where(entry => entry.DaysOfWeek is DaysOfWeek.WednesdayAndSaturday).ToFrozenSet(); public static DaysOfWeek GetDaysOfWeek(MaterialId id) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/RotationalMaterialIdEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/RotationalMaterialIdEntry.cs new file mode 100644 index 0000000000..810a33c26d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/RotationalMaterialIdEntry.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.ViewModel.Cultivation; +using System.Collections.Frozen; + +namespace Snap.Hutao.Model.Metadata.Item; + +internal sealed class RotationalMaterialIdEntry +{ + private readonly FrozenSet idSet; + + public RotationalMaterialIdEntry(DaysOfWeek daysOfWeek, MaterialId green, MaterialId blue, MaterialId purple, MaterialId orange) + : this(daysOfWeek, green, blue, purple) + { + DaysOfWeek = daysOfWeek; + Orange = orange; + Purple = purple; + Blue = blue; + Green = green; + + idSet = FrozenSet.ToFrozenSet([green, blue, purple, orange]); + } + + public RotationalMaterialIdEntry(DaysOfWeek daysOfWeek, MaterialId green, MaterialId blue, MaterialId purple) + { + DaysOfWeek = daysOfWeek; + Purple = purple; + Blue = blue; + Green = green; + + idSet = FrozenSet.ToFrozenSet([green, blue, purple]); + } + + public DaysOfWeek DaysOfWeek { get; } + + public MaterialId Orange { get; } + + public MaterialId Purple { get; } + + public MaterialId Blue { get; } + + public MaterialId Green { get; } + + public MaterialId Highest { get => Orange != 0U ? Orange : Purple; } + + public FrozenSet Set { get => idSet; } + + public IEnumerable Enumerate() + { + yield return Green; + yield return Blue; + yield return Purple; + + if (Orange != 0U) + { + yield return Orange; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs index a0191df28a..2e17f635bc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs @@ -70,10 +70,12 @@ public ICalculableWeapon ToCalculable() return CalculableWeapon.From(this); } - public Model.Item ToItem() + public TItem ToItem() + where TItem : Model.Item, new() { return new() { + Id = Id, Name = Name, Icon = EquipIconConverter.IconNameToUri(Icon), Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType), @@ -85,6 +87,7 @@ public StatisticsItem ToStatisticsItem(int count) { return new() { + Id = Id, Name = Name, Icon = EquipIconConverter.IconNameToUri(Icon), Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType), @@ -97,6 +100,7 @@ public SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp { return new() { + Id = Id, Name = Name, Icon = EquipIconConverter.IconNameToUri(Icon), Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType), diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index 2df305c240..836c75a457 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -13,7 +13,7 @@ + Version="1.11.8.0" /> Snap Hutao diff --git a/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest index d02c82aadf..5014d3ec8a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest @@ -13,7 +13,7 @@ + Version="1.11.8.0" /> Snap Hutao Dev diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx index 7ec895678f..8fbf575e6a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx @@ -670,6 +670,15 @@ Multiple identical achievement IDs found in a single achievement archive + + [Listing Time] + + + Open forever + + + Train update time + ATK Need EXACT same string in game @@ -743,7 +752,7 @@ Need EXACT same string in game - Heal bonus + Incoming Healing Bonus HP @@ -782,7 +791,7 @@ Need EXACT same string in game - Cooldown reduction + CD Reduction Enhancement Progression Calculator: N/A @@ -929,6 +938,9 @@ Can't find available URL + + Game is running, please close the game and retry + Invalid URL provided @@ -1014,13 +1026,13 @@ Game path not found in Unity log file - Inventory data parse failed + Manifest parse failed - Not enough disk space, {0}is required, {1} left + Insufficient disk space, need {0}, remaining {1} - Installing Games + Installing Game Pre-downloading resources @@ -1086,7 +1098,7 @@ Error reading process modules' memory: could not read valid value in given address - Failed to unlock frame rate:x{0:X8} + Failed to unlock frame rate:0x{0:X8} Wish history data backup service will expire at \n{0:yyyy.MM.dd HH:mm:ss} @@ -1145,26 +1157,29 @@ Can't get user information with entered Cookie + + Retrieving user information, please wait... + - Append Attribute Recommendation + Additional Property Recommendation - Rational Champion Attribute Recommendations + Circlet of Logos Property Recommendation - Empty Grail Property Recommendation + Goblet of Eonothem Property Recommendation - Sand Properties Recommendations + Sands of Eon Property Recommendation Copy Image - Fetching inventory data + Fetching manifest - Please complete authentication + Please complete the verification Achievements @@ -1308,16 +1323,16 @@ Add or update to current Enhancement Progression Plan - Always create new adopted target items + Always create new entries Save Method - Overwrite existing adopted items + Overwrite existing entries - Keep existing target items + Keep existing entries Daily Commission Availability Notification @@ -1376,6 +1391,30 @@ Input wish history URL manually + + Game Server + + + Currently selected directory is HDD, SSD is recommended + + + Set the game installation directory + + + Install Game + + + Chinese + + + English + + + Japanese + + + Korean + Input request Url with composit template @@ -1505,6 +1544,15 @@ Update + + Please enter an account + + + Enter your password + + + Login to your account + Take me there @@ -1676,6 +1724,9 @@ Character showcase is disabled. Please activate it in the game. + + {0} characters in total + Failed to copy role details @@ -1701,7 +1752,7 @@ No plan has been created and selected - There is already a foster project for this item + There is already a cultivation entry for this item Successfully added to current plan @@ -1821,7 +1872,7 @@ Local version: {0} - Pre-download for {0} is on + {0} Version pre-download is now available Latest version: {0} @@ -1833,19 +1884,19 @@ Canceling - Installation completed + Installation complete - Predownload completed + Pre-download complete - Fix Completed + Fix complete - Update completed + Update complete - The game is complete, no need to fix + The game integrity is intact, no need to fix I've Read and Agreed Conditions Above @@ -1880,9 +1931,15 @@ Import failed + + The currently selected user and game server do not match. Please re-select + The currently selected game directory {0} is corrupted. Please manually delete the folder and reinstall the game + + Login failed with MiYouShe/HoYooLAB user. Please login again + Convert server failed @@ -1953,7 +2010,7 @@ Failed to save game path - The current data directory `{0}' contains one or more reanalytics points (Reparse points) that may cause some data folders to work properly. Please go to set the change path + The current data directory '{0}' contains one or more reparse points, which may cause some of the functions that depend on the data folder to not work properly, please go to Settings to change the path. Export failed @@ -2025,7 +2082,7 @@ Export - Charged achievements only + Daily commission achievements only Import from Clipboard @@ -2139,7 +2196,7 @@ Material Checklist - Unbundled priority + Uncollected First Material Statistics @@ -2232,7 +2289,7 @@ Do Not Disturb - Long Effect Points + Long-Term Encounter Points Verify Current User and Role @@ -2555,6 +2612,15 @@ Automated Tasks + + Enable this option when error code 31-4302 + + + Let's go in! + + + Cannot switch again after game starts + General @@ -2579,9 +2645,27 @@ Discord Activity + + Game file management/switching the game server/injection feature requires admin permission + File + + Fix the horizon for special interfaces such as roles/team configuration/prayer / walk-in + + + Special Interface Fix + + + Real-time switches after game starts + + + Inject the message module into the game in order to perform some advanced but dangerous features + + + Injection + Process Linkage @@ -2601,7 +2685,7 @@ Game Options - Install Games + Install Game Pre-download @@ -2627,6 +2711,15 @@ Registry + + Team configuration cold switch + + + Remove the progress bar when opening the team configuration screen + + + Remove Team Configuration Progress Bar + OTA Package @@ -2672,12 +2765,27 @@ You need to convert to a server that matches the launcher before updating the version + + Cooling Switch to View + - Adjust Camera Vision, Default 45 + Adjust FOV, Default 45 Adjust FOV + + Field Heat Switches + + + Target Frame Rate + + + Frame Thermal Switches + + + Hover the mouse over the feature switch/text box to see details + Please disable the "Vertical Sync" option inside the game, which requires a high-performance GPU to support higher frame rate, and set the value to -1 for unlimited framers. @@ -2702,6 +2810,12 @@ Enabled + + Sign in with the currently selected meters / HoYoLAB user. Start parameters need to be enabled + + + Sign in with MiYouShe/HoYoLAB user + Take advantage of monitors that support HDR for brighter, more vivid, and more detailed pictures @@ -2981,6 +3095,9 @@ Open Background Folder + + Convert Server Mode + Reset @@ -3011,6 +3128,12 @@ When setting the game path, please select the game program (Yuanshen.exe or GenshinImpact.exe) instead of the game launcher (launcher.exe) + + Select the mode in which to save the image after clicking the share button + + + Share Save Mode + Shell Experience @@ -3071,6 +3194,9 @@ Webview2 Runtime + + WebView + Second Half @@ -3113,6 +3239,9 @@ Other + + Contact Card + Occupation @@ -3281,6 +3410,12 @@ Official Tools + + Login + + + Trilateral Login + Web Login @@ -3351,7 +3486,7 @@ Weapon WIKI - Take a screenshot of this window, find a suitable feedback path in the feedback center and send it to the UNDG. + Please take a screenshot of this window and find the appropriate feedback channel in the Feedback Center and send it to the development team. An unrecoverable fatal error has occurred @@ -3383,6 +3518,21 @@ Copied to clipboard + + Save shared image + + + Image saving failed + + + Image saved successfully + + + Copy to clipboard + + + Save as file + All Archon Quest Completed diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.fr.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.fr.resx index 55e490d6dc..364175b4b9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.fr.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.fr.resx @@ -670,6 +670,15 @@ 单个成就存档内发现多个相同的成就 Id + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + 攻击力 Need EXACT same string in game @@ -929,6 +938,9 @@ 未找到可用的 Url + + 游戏正在运行中,关闭游戏后重试 + 提供的 Url 无效 @@ -1145,6 +1157,9 @@ 输入的 Cookie 无法获取用户信息 + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1376,6 +1391,30 @@ 手动输入祈愿记录 Url + + 游戏服务器 + + + 当前选择目录为HDD,建议使用SSD + + + 设置游戏安装目录 + + + 安装游戏 + + + 汉语 + + + 英语 + + + 日语 + + + 韩语 + 请输入请求接口的 Url 复合模板 @@ -1505,6 +1544,15 @@ 更新 + + 请输入账号 + + + 请输入密码 + + + 账号密码登录 + 立即前往 @@ -1676,6 +1724,9 @@ 角色展柜尚未开启,请前往游戏操作后重试 + + 共 {0} 位角色 + 复制角色详情失败 @@ -1880,9 +1931,15 @@ 导入失败 + + 当前选中用户和游戏服务器不匹配,请重新选择 + 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 + + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 + 切换服务器失败 @@ -2555,6 +2612,15 @@ 自动化任务 + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + 常规 @@ -2579,9 +2645,27 @@ Discord Activity + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + 文件 + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + 进程联动 @@ -2627,6 +2711,15 @@ 注册表 + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + 增量包 @@ -2672,12 +2765,27 @@ 版本更新前需要提前转换至与启动器匹配的服务器 + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2702,6 +2810,12 @@ 启用 + + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 + + + 使用 米游社 / HoYoLAB 用户登录 + 充分利用支持高动态范围的显示器获得更亮、更生动、更精细的画面 @@ -2981,6 +3095,9 @@ 打开背景图片文件夹 + + 转换服务器模式 + 重置 @@ -3011,6 +3128,12 @@ 设置游戏路径时,请选择游戏本体(YuanShen.exe 或 GenshinImpact.exe)而不是启动器(launcher.exe) + + 选择点击分享按钮后保存图片的模式 + + + 分享保存模式 + Shell 体验 @@ -3071,6 +3194,9 @@ Webview2 运行时 + + WebView + 下半 @@ -3113,6 +3239,9 @@ 其它 + + 名片 + 所属 @@ -3281,6 +3410,12 @@ 旅行工具 + + 账密登录 + + + 三方登录 + 网页登录 @@ -3383,6 +3518,21 @@ 已复制到剪贴板 + + 保存分享图片 + + + 图片保存失败 + + + 图片保存成功 + + + 复制到剪贴板 + + + 保存为文件 + 所有魔神任务已完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.id.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.id.resx index 31073e6917..f960e0b4e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.id.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.id.resx @@ -670,6 +670,15 @@ Terdapat beberapa ID pencapaian yang identik dalam satu arsip pencapaian + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + ATK Need EXACT same string in game @@ -929,6 +938,9 @@ Tidak dapat menemukan URL yang tersedia + + 游戏正在运行中,关闭游戏后重试 + URL yang diberikan tidak valid @@ -1145,6 +1157,9 @@ Tidak bisa mendapatkan informasi pengguna dengan Cookie yang dimasukkan + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1376,6 +1391,30 @@ Masukan histori wish URL secara manual + + 游戏服务器 + + + 当前选择目录为HDD,建议使用SSD + + + 设置游戏安装目录 + + + 安装游戏 + + + 汉语 + + + 英语 + + + 日语 + + + 韩语 + Masukkan URL permintaan dengan template Composit @@ -1505,6 +1544,15 @@ 更新 + + 请输入账号 + + + Masukkan kata sandi + + + 账号密码登录 + Tuju saya kesana @@ -1676,6 +1724,9 @@ Pameran karakter dinonaktifkan. Silakan aktifkan di dalam permainan. + + 共 {0} 位角色 + 复制角色详情失败 @@ -1880,9 +1931,15 @@ Gagal Impor + + 当前选中用户和游戏服务器不匹配,请重新选择 + 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 + + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 + Konversi server gagal @@ -2555,6 +2612,15 @@ 自动化任务 + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + Umum @@ -2579,9 +2645,27 @@ Aktifitas Discord + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + Berkas + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + InterProcess @@ -2627,6 +2711,15 @@ Registri + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + Paket OTA @@ -2672,12 +2765,27 @@ Anda perlu mengonversi ke server yang sesuai dengan peluncur sebelum memperbarui versi + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2702,6 +2810,12 @@ Aktifkan + + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 + + + 使用 米游社 / HoYoLAB 用户登录 + Manfaatkan tampilan yang mendukung rentang dinamis tinggi untuk gambar yang lebih terang, lebih jelas, dan lebih detail @@ -2981,6 +3095,9 @@ Buka Berkas Latar Belakang + + 转换服务器模式 + Reset @@ -3011,6 +3128,12 @@ Saat mengatur jalur permainan, pilih program permainan (Yuanshen.exe atau GenshinImpact.exe) bukan peluncur permainan (launcher.exe) + + 选择点击分享按钮后保存图片的模式 + + + 分享保存模式 + Pengalaman Shell @@ -3071,6 +3194,9 @@ Webview2 Runtime + + WebView + 下半 @@ -3113,6 +3239,9 @@ Lainnya + + 名片 + Pekerjaan @@ -3281,6 +3410,12 @@ Official Tools + + 账密登录 + + + 三方登录 + Web Login @@ -3383,6 +3518,21 @@ Disalin ke clipboard + + 保存分享图片 + + + 图片保存失败 + + + 图片保存成功 + + + 复制到剪贴板 + + + 保存为文件 + 所有魔神任务已完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx index f5a514fa82..80bd9e1607 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx @@ -190,7 +190,7 @@ 胡桃がリアルタイムノートを更新するために使用するタスクです。編集や削除をしないでください! - セマフォが解放され、操作がキャンセルされました + セマフォが既に解放され、操作が取り消されました WebView2 ランタイムが検出されませんでした @@ -641,22 +641,22 @@ 無効なUIDです。 - 深境螺旋のデータをアップロードに失敗しました。現行の期間のデータではありません。 + 深淵の記録をアップロードに失敗しました。現行の期間のデータではありません。 深境螺旋のデータをアップロードに失敗しました。現在の UID の記録はまだ処理中であるため、操作を繰り返さないでください。 - 深境螺旋のデータをアップロードしました。胡桃クラウドの利用期間延長特典を受け取りました。 + 深淵の記録をアップロードしました。胡桃サービスの祈願保存期限を受け取りました。 - 深境螺旋のデータをアップロードしましたが、胡桃クラウドへログインしていないため、利用期間特典を受け取ることは出来ません。 + 深淵の記録をアップロードしましたが、胡桃通行証へログインしていないため、祈願保存期限を受け取りませんでした。 - 深境螺旋のデータをアップロードしましたが、ユーザーが見つからなかったため、利用期間特典を受け取ることができませんでした。 + 深淵の記録をアップロードしましたが、ユーザーが見つからなかったため、祈願保存期限を受け取りませんでした。 - 深境螺旋のデータをアップロードしました。今期は二回以上アップロードしているため、利用期間特典を受け取ることは出来ません。 + 深淵の記録をアップロードしましたが、今期の初回アップロードではないため、祈願保存期限を受け取りませんでした。 {0} つのアチーブメントを追加 | {1} つのアチーブメントを更新 |{2} つのアチーブメントを削除 @@ -670,6 +670,15 @@ 単一のアーカイブに同一のアチーブメント ID が複数存在しています + + 【リリース時間】 + + + 期限なし + + + 〓更新時期〓 + 攻撃力 Need EXACT same string in game @@ -929,6 +938,9 @@ 利用可能なUrlが見つかりません + + ゲームは実行中で、一旦ゲームを閉じてから再試行してください。 + 無効なURLです @@ -1146,6 +1158,9 @@ ユーザー情報を取得できません + + ユーザー情報を取得しています、しばらくお待ちください... + 追加ステータス推奨 @@ -1183,7 +1198,7 @@ アチーブメント集計 - すべてのアイテムのコピーが利用可能になるまで + すべての秘境が挑戦できます 誕生日を迎えるキャラはいません @@ -1234,7 +1249,7 @@ UP 獲得平均数 - ピックアップ + UP 予測 @@ -1377,6 +1392,30 @@ 祈願履歴 URL を手動で入力する + + ゲームサーバー + + + 現在選択されているディレクトリは HDD ですが、SSD を推奨します + + + ゲームのインストール場所を設定 + + + ゲームをインストール + + + 中国語 + + + 英語 + + + 日本語 + + + 韓国語 + リクエスト URL の複合テンプレートを入力してください @@ -1506,6 +1545,15 @@ アップデート + + アカウントを入力してください + + + パスワードを入力してください + + + アカウントやパスワードでログインする + すぐに移動 @@ -1612,7 +1660,7 @@ 深境螺旋集計 - すべての通知をクリア + すべて消去 折りたたむ @@ -1677,6 +1725,9 @@ キャラクターラインナップが未配置か非表示です。ゲーム内のプロフィール編集で設定してください + + キャラ総計 {0} 人 + キャラ詳細をコピーに失敗しました @@ -1881,9 +1932,15 @@ インポート失敗 + + 現在選択されているユーザーとサーバーが一致しません。もう一度選択してください。 + 現在選択されているゲームパス {0} が壊れています。フォルダーを手動で削除しゲームを再インストールしてください + + 米游社 / HoYoLAB ユーザーでログインに失敗しました。もう一度ログインしてください + サーバーの切り替えができませんでした @@ -1954,16 +2011,16 @@ ゲームパスの保存に失敗しました - データディレクトリ'{0}'には1つまたは複数のアナリティクスが存在します。一部の依存関係データフォルダが正常に機能しませんので経路を変更してください。 + 現在のデータディレクトリ '{0}' には1つ以上の再解析ポイント (Reparse points) が含まれている為、データフォルダに依存する一部の機能が正常に動作しなくなるかもしれない故、設定へパスを変更してください! エクスポート失敗 - エクスポートに成功しました + エクスポート成功 - UID の重複を避けるために、インポートされたUIGFファイルには UID の重複送信 + インポートされた UIGF ファイルには、UID が重複した祈願履歴が含まれています インポート失敗 @@ -1972,7 +2029,7 @@ インポートされた UIGFファイルには、祈願データが含まれていません。 - データをインポートするには UID を指定する必要があります。 + データをインポートするには少なくとも UID を1つ選択してください インポート成功 @@ -1984,10 +2041,10 @@ ユーザー [{0}] の Cookie をコピーしました - このCookieが不完全のため、操作できません + この Cookie が不完全で、操作できません - このCookieが無効のため、操作できません + この Cookie が無効ので、操作できません ユーザー [{0}] を削除しました @@ -1996,10 +2053,10 @@ ユーザー [{0}] の Cookie が更新されました。 - ビューリソースが解放され、操作がキャンセルされました + ビューリソースが既に解放され、操作が取り消されました - 胡桃を利用できるようになりました + これで胡桃が使えるようになりました! ダウンロード完了 @@ -2008,7 +2065,7 @@ 完了 - レスポンスストリームに有効なコンテンツタイプが含まれていない + レスポンスストリームが有効なコンテンツタイプではありません 待機中 @@ -2020,7 +2077,7 @@ 新規アーカイブ - 続けるにはアーカイブを作成してください + まず「新規アーカイブ」を作成して続けよう! エクスポート @@ -2044,7 +2101,7 @@ 実績名 · 説明 ·バージョン· 番号で検索 - 未達成順にソート + 未達成に優先 イベント @@ -2077,7 +2134,7 @@ テキストをクリップボードにコピー - キャラクターの元素タイプ + ステータス情報 初期付与のサブOP @@ -2122,13 +2179,13 @@ 新規 - 続けるには、まず「育成計画」を立てよう + まず「新規育成計画」を作成して続けよう! 育成計画の項目は後から他のページでも追加できます。 - マイ キャラクターと武器を育成計画に追加します + マイキャラクターと武器を育成計画に追加します 育成素材 @@ -2140,7 +2197,7 @@ 素材リスト - 未集束優先 + 収集未完成に優先 素材情報 @@ -2158,13 +2215,13 @@ 現在の育成計画を削除 - キャラクターの育成計画を追加する + 任意のキャラを育成計画に追加する - 武器の育成計画を追加する + 任意の武器を育成計画に追加する - リアルタイムのメモを追加 + リアルタイムノートを追加 リアルタイムノートを追加して定期的に更新できます @@ -2173,7 +2230,7 @@ 追加 - 冒険ポイントのステータス + 修練ポイント情報 リアルタイムノートが更新後に指定の Webhook へデータを送信します。 @@ -2200,7 +2257,7 @@ 更新間隔 - 通知をクリックするまで画面上に表示したままにします。 + 通知が操作センターに自動的収まる事を防ぎます リマインド通知 @@ -2209,7 +2266,7 @@ カードを削除 - 今週の消費半減は消化済 + 今週の残り消費半減回数 自動更新 @@ -2218,7 +2275,7 @@ 指定間隔でリアルタイムノートを更新します - これらの設定は管理者モードでない時のみ変更できます。 + これらのオプションは非管理者モードでのみ変更できます。 更新 @@ -2230,13 +2287,13 @@ プレイ中に通知をオフ - おやすみモード + 集中モード - 長効率練点 + 長期修練ポイント - 現在のユーザーとUIDを確認する + 現在のユーザーとキャラを認証 質問や提案を検索 @@ -2257,16 +2314,16 @@ 解除済み - ループバック制限解除の設定 + Loopback 制限を解除する - 引き続き連絡をしてください + 引き続き連絡を取り合いましょう 機能ガイド - Githubでは報告された問題を常に優先しています + GitHub で報告された問題には常に優先順位をつけています 開発ロードマップ @@ -2275,13 +2332,13 @@ 検索結果はありません - 胡桃のサービス状態 + 胡桃サービス可用性の監視モニター 胡桃サービス - すべて更新 + 完全更新 エクスポート @@ -2302,7 +2359,7 @@ このUIDのクラウドバックアップを削除する - デベロッパーアカウントは利用期限がありません + 開発者アカウントは無期限にサービスを利用できます 胡桃クラウドのサービス期限が切れました。 @@ -2311,7 +2368,7 @@ このUidのクラウドバックアップをダウンロードする - 各シーズンの深境螺旋の記録を初めてアップロードすると3日間のフリーライセンスが付与されます。 + 毎期の深境螺旋記録を初回アップロードすると、胡桃クラウドの使用が3日間無料になります。 螺旋の記録をアップロード @@ -2341,25 +2398,26 @@ 情報更新 - フェッチ + 取得 - Urlを入力する + 手動で URL を入力 - 旅人が用意したUrlで祈願履歴を更新 + 旅人さんが提供した URLで祈願履歴を更新します - SToken 更新 + SToken で更新 - 現在のユーザーのCookieで祈願履歴を表示する + 現在のユーザーの Cookie で祈願履歴を更新します + - ゲーム内ブラウザキャッシュで更新 + ウェブキャッシュで更新 - ゲーム内ブラウザのキャッシュで祈願履歴を更新 + ゲーム内ブラウザーのキャッシュで祈願履歴を更新します 現在のアーカイブを削除する @@ -2374,7 +2432,7 @@ 一覧 - 統計 + グローバル祈願統計 武器 @@ -2383,7 +2441,7 @@ 胡桃はあなたのためにゲームを {0} 回起動しました - 胡桃を {0} 回起動しました + あなたは胡桃を {0} 回起動しました 旅人、テイワットへようこそ! @@ -2479,7 +2537,7 @@ アカウント作成 - パスワードを忘れました + パスワードをリセット 削除されたアカウントのデータは永久的に削除され、復元することは出来ません。 @@ -2497,7 +2555,7 @@ ゲームスタート - 上級者向け設定 + 高度な機能 指定した解像度に素早く切り替えます @@ -2515,10 +2573,10 @@ ボーダーレス - タッチパネル レイアウトを有効化します。キーボードとマウスは使えなくなります。 + 内蔵のタッチレイアウトを有効し、キーボードとマウスは使えなくなります。 - ゲーム内ブラウザには対応していません。ウィンドウの切り替え操作などによりゲームが強制終了する可能性があります。 + ゲーム内の埋め込みブラウザとの互換性がなく、ウィンドウ切り替えなどの操作でゲームが強制終了することがあります。 排他的フルスクリーン @@ -2533,22 +2591,22 @@ 外観 - ゲームのウィンドウの高さを上書き + ゲームのウィンドウの高さを上書きします。 高さ - ゲームのウィンドウの幅を上書き + ゲームのウィンドウの幅を上書きします。 - ゲーム開始時の動作を変更します。 + ゲーム開始時のデフォルト動作を変更します。 - コマンドラインパラメーター + 起動のパラメーター ゲーム起動後 Better GI を開始してみ、自動化任務しようとします @@ -2556,6 +2614,15 @@ 自動化任務 + + エラーコード 31-4302 があった場合はこのオプションを有効にしてください + + + 入らせて! + + + ゲームの起動後に変更できません。 + 一般 @@ -2563,31 +2630,49 @@ これらの設定はゲームが正常に起動した時のみ保存されます。 - 光の描画から霧を削除 + 照明レンダリングの霧を除去する - チームを削除 + 霧を取り除く - 予約済み + 保留 削除 - ゲームをプレイしている時に、Discord Activityのステータスを変更します。 + ゲーム中に Discord Activity のステータスを設定します。 Discord Activity + + ゲームファイル管理/サーバーの切り換え/注入機能には管理者権限が必要です + ファイル + + キャラクター/チーム編成/祈願/紀行などの特殊シーンを補正します + + + 特殊シーンの補正 + + + ゲーム起動後にリアルタイムで切り替えることができます + + + メッセージモジュールをゲームに注入し、高度だが危険な機能を実現します + + + 注入 + インタープロセス - 指定したディスプレイで実行 + 指定したディスプレイで実行します。 モニター @@ -2611,13 +2696,13 @@ 事前ダウンロードは完成しました - ゲームを更新します + ゲームを更新 ゲームを修復 - ゲームの開始後にStarward ランチャーを起動し、プレイ時間の統計を確認してみてください。 + ゲームの開始後に Starward を起動してみ、プレイ時間の統計を行おうとします。 プレイ時間 @@ -2628,29 +2713,38 @@ レジストリ + + チーム編成コールドスイッチ + + + チーム編成画面を開いたときに表示される進捗バーを取り除きます + + + チーム編成の進捗バーを取り除く + - 増分パック + 差分パッケージ リソースダウンロード - クライアント + 完全パッケージ 事前ダウンロード - ゲームのフォルダを選択 + ゲームのパスを選択 - このアカウントはリアルタイムノート通知 UID として連携されていません。 + このアカウントはリアルタイムノート通知 UID に連携されていません。 - 現在のユーザのUIDを連携する + 現在のユーザー UID を連携する - ゲーム内でアカウントを切り替えたり、インターネット環境を変更した場合は再検出が必要です。 + ゲーム内でアカウントを切り替える場合、ネットワーク環境が変わった後に手動で再検出する必要があります。 検出 @@ -2673,26 +2767,41 @@ バージョンを更新する前に、ランチャーと一致するサーバーに切り換える必要があります。 + + 視野コールドスイッチ + カメラビューを45まで調整する 視界を調整 + + 視野ホットスイッチ + + + ターゲットFPS + + + FPS ホットスイッチ + + + マウスカーソルを機能スイッチ/テキストボックスに合わせると詳細が表示されます + - FX同期を無効にするには、より高パフォーマンスなグラフィックカードをサポートするために、ゲーム内のFPSの設定と -1を設定すると、すべてのフレームレートは無制限に設定されます。 + より高い FPS をサポートするために高性能な GPU を必要とします。ゲーム内の「垂直同期」を無効にし、無制限のフレームレートのために値を -1 に設定してください。 フレームレート上限解除 - ロック解除フレームレート変更方法の変更 + フレームレートの解除方法を変更 - ロック解除モード + 解除方法 - エラー コード31-4302。このオプションを有効にした場合、グラデーションと霧の調整が無効になり、ゲームの起動前に切り替わるようにできます。 + エラーコード 31-4302 が発生した場合はこのオプションを有効にします。有効にすると、視野と霧の調整が無効になり、ゲームを開始する前にこのスイッチを切り替えた場合にのみ有効になります 互換モード @@ -2703,14 +2812,20 @@ 有効 + + 今は選ばれている 米游社 / HoYoLAB ユーザーを使用してログインします。「起動のパラメーター」を有効にする必要があります。 + + + 米游社 / HoYoLAB ユーザーでログイン + - HDRをサポートするディスプレイを活用して、より明るく鮮やかなグラフィックを実現します。 + HDR に対応したモニターを活用すれば、より明るく、より鮮やかで、より精細な画像が得られます Windows HDR - HoYoLab UIDを入力してください + HoYoLAB UIDを入力してください あなたが[MiHoYo 通行証アカウント]にログインするために埋め込みウェブビューを使用しており、[サインしました]ボタンをクリックするとこのクライアントが貴方の Cookie データを取得します。このウェブビューで開始されたすべてのネットワーク通信は、あなたのコンピュータと MiHoYo 公式サーバーの間でのみ行われます。 @@ -2719,7 +2834,7 @@ ログインしました - MiHoYo BBS通行証でログイン + 下のウィンドウから MiHoYo 通行証でログインします スクリーンショットフォルダを開く @@ -2767,7 +2882,7 @@ イメージキャッシュはここに格納されます - キャッシュフォルダ + キャッシュ フォルダー 胡桃通行証 @@ -2779,7 +2894,7 @@ 新規作成 - 管理者として実行できるショートカットを作成します + 管理者権限でデスクトップショートカットを作成します ショートカットを作成する @@ -2788,31 +2903,31 @@ 実行 - 潜在的に危険な機能 + 危険な機能 デベロッパーが明確に指示しない限り、以下の機能を有効にすべきではありません。 - ユーザーデータ/メタデータはここに格納 + ユーザーデータ / メタデータ ここに保存 - データフォルダ + データ フォルダー 削除 - 祈願履歴を更新した際、検証キーの有効期限が切れた等のエラーが表示された場合は、この操作を行うと良いでしょう。 + 祈願履歴の更新時に、認証キーの有効期限切れエラーが頻繁に発生する場合は、この操作をお試しください。 ゲーム内ブラウザキャッシュを削除する - ユーザーテーブルに保存されている記録を削除し、特定のアカウントの競合などの問題を修正します。 + ユーザーテーブルに保存されている記録を全部削除し、特定のアカウントの競合などの問題を修正します。 - 保存されたすべてのユーザーを削除 + すべてのユーザーを削除する デバイス ID @@ -2821,7 +2936,7 @@ IP: {0} サーバー: {1} - デバイスのIP + デバイス IP ダウンロード @@ -2836,10 +2951,10 @@ 管理者モードで再起動 - 祈願履歴のないイベント限定祈願を祈願履歴に表示するかを変更します。 + 祈願履歴のない過去の祈願を表示または隠します - 祈願を行っていない過去のイベント祈願を表示する + 無記録の過去の祈願 非表示 @@ -2848,7 +2963,7 @@ 表示 - 利用規則に違反する可能性のある機能『ゲームランチャー - 上級者向け設定』を有効にしました。ユーザーご自身がそれによって生じる結果について、一切の責任を負うものとなります。 + あなたは原神の利用規約に違反する可能性のある「ゲームランチャー - 高度な機能」をアンロックしています。いかなる不利な結果に対してもご自身で責任を負うものとなります。 フィードバック @@ -2920,7 +3035,7 @@ 胡桃アカウント - 胡桃クラウドのサービスをベースにした様々な機能が制限なく利用できます + 胡桃クラウドのサービスに基づくあらゆる機能が制限なく利用できます。 認定済みの共同開発者 @@ -2932,16 +3047,16 @@ ログアウト - テスト段階のあらゆる機能を使用できます。 + あらゆるテスト機能を制限なく使用できます。 - 胡桃の運用と開発または保守 + 胡桃の開発 / 保守 - 胡桃クラウドの引き替えコードを一部のユーザーに配布する事があります。 + 胡桃クラウドの引き換えコードを一部のユーザーに配布することがあります。 - 引き替えコードの使用 + 引き換えコードの使用 登録 @@ -2953,10 +3068,10 @@ アカウントの削除 - 原神およびSnap Hutaoの利用規約を全て熟読し、その後に『ゲームランチャー - 上級者向け設定』を有効にします。 + 私は原神及び Snap Hutao の利用規約を全て熟読し、その後に「ゲームランチャー - 高度な機能」を有効にします。 - 上級者向け設定を有効にする + 高度な機能 オートクリック機能のショートカットキーを変更します @@ -2977,11 +3092,14 @@ 公式サイト - カスタム背景を設定する / bmp、git、ico、jpg、jpeg、png、tiff、webp形式をサポートしています + カスタム背景画像、 bmp / gif / ico / jpg / jpeg / png / tiff / webp 形式をサポートしています カスタム背景フォルダーを開く + + サーバーモードの変換 + リセット @@ -3010,7 +3128,13 @@ ゲーム本体の場所を開く - ゲームのパスを設定する際、本体(YuanShen.exe または GenshinImpact.exe)を選んでください。ランチャー(launcher.exe)ではありません + ゲームパスを設定する際、ランチャー(launcher.exe)ではなく、本体(YuanShen.exe または GenshinImpact.exe)を選択してください。 + + + 共有ボタンをクリックした後、画像を保存するモードを選択します + + + 共有の保存モード Shell エクスペリエンス @@ -3072,6 +3196,9 @@ Webview2 ランタイム + + WebView + 後半 @@ -3079,7 +3206,7 @@ 前半 - おすすめ聖遺物 + 推奨組み合わせ 育成素材 @@ -3091,7 +3218,7 @@ 中国語CV - 命の星座 + 命ノ星座 コスチューム @@ -3114,6 +3241,9 @@ その他 + + 名刺 + 所属 @@ -3282,6 +3412,12 @@ 旅行ツール + + アカウント ログイン + + + サードパーティ ログイン + ウェブ上でログイン @@ -3364,7 +3500,7 @@ 実行 - デバッグ スクリプトを実行します。 + デバッグスクリプトを実行 {0} 日後に開始 @@ -3384,6 +3520,21 @@ クリップボードにコピーしました。 + + 共有画像を保存 + + + 画像の保存に失敗しました + + + 画像を保存しました。 + + + クリップボードにコピー + + + ファイルとして保存 + すべての魔神任務が完了しました @@ -3424,7 +3575,7 @@ {0} 時間 {1} 分 - すべての依頼を完了していません + 今日の依頼完了数が足りません 依頼報酬はまだ受取っていません @@ -3541,10 +3692,10 @@ 無効なUIDです - 中国サーバー: 公式 + 中国サーバー: 天空島 - 中国サーバー: ビリビリ(bilibili) + 中国サーバー: 世界樹 国内サーバー: アジア diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx index bd8186fc97..1deeaf917d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx @@ -670,6 +670,15 @@ 한 업적 아카이브에서 Id 동일한 업적 발견됨 + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + 공격력 Need EXACT same string in game @@ -929,6 +938,9 @@ 사용 가능한 Url이 없습니다 + + 游戏正在运行中,关闭游戏后重试 + 제공된 Url이 잘못되었습니다 @@ -1145,6 +1157,9 @@ 입력한 쿠키에서 사용자 정보를 얻을 수 없습니다 + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1376,6 +1391,30 @@ 수동 기원 기록 입력 Url + + 游戏服务器 + + + 当前选择目录为HDD,建议使用SSD + + + 设置游戏安装目录 + + + 安装游戏 + + + 汉语 + + + 英语 + + + 日语 + + + 韩语 + 请输入请求接口的 Url 复合模板 @@ -1505,6 +1544,15 @@ 更新 + + 请输入账号 + + + 비밀번호를 입력하세요 + + + 账号密码登录 + 지금 이동 @@ -1676,6 +1724,9 @@ 캐릭터 상세정보 보기가 꺼져있습니다. 게임에서 설정 후 다시 시도하십시오. + + 共 {0} 位角色 + 复制角色详情失败 @@ -1880,9 +1931,15 @@ 가져오기 실패 + + 当前选中用户和游戏服务器不匹配,请重新选择 + 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 + + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 + 서버 변경 실패 @@ -2555,6 +2612,15 @@ 自动化任务 + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + 보통 @@ -2579,9 +2645,27 @@ Discord Activity + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + 文件 + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + 进程间 @@ -2627,6 +2711,15 @@ 注册表 + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + 증분 패키지 @@ -2672,12 +2765,27 @@ 版本更新前需要提前转换至与启动器匹配的服务器 + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2702,6 +2810,12 @@ 활성화 + + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 + + + 使用 米游社 / HoYoLAB 用户登录 + 充分利用支持高动态范围的显示器获得更亮、更生动、更精细的画面 @@ -2981,6 +3095,9 @@ 打开背景图片文件夹 + + 转换服务器模式 + 초기화 @@ -3011,6 +3128,12 @@ 게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요 + + 选择点击分享按钮后保存图片的模式 + + + 分享保存模式 + Shell 体验 @@ -3071,6 +3194,9 @@ Webview2 런타임 + + WebView + 下半 @@ -3113,6 +3239,9 @@ 其它 + + 名片 + 所属 @@ -3281,6 +3410,12 @@ 旅行工具 + + 账密登录 + + + 三方登录 + 웹 로그인 @@ -3383,6 +3518,21 @@ 已复制到剪贴板 + + 保存分享图片 + + + 图片保存失败 + + + 图片保存成功 + + + 复制到剪贴板 + + + 保存为文件 + 所有魔神任务已完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.pt.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.pt.resx index 9feb1720fe..a6a3fd64d4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.pt.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.pt.resx @@ -670,6 +670,15 @@ Várias IDs de conquistas idênticas encontradas em um único arquivo de conquistas + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + ATQ Need EXACT same string in game @@ -929,6 +938,9 @@ Não é possível encontrar o URL disponível + + 游戏正在运行中,关闭游戏后重试 + URL inválido fornecido @@ -1145,6 +1157,9 @@ Não é possível obter informações do usuário com o cookie inserido + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1376,6 +1391,30 @@ Insira manualmente o URL do histórico de orações + + 游戏服务器 + + + 当前选择目录为HDD,建议使用SSD + + + 设置游戏安装目录 + + + 安装游戏 + + + 汉语 + + + 英语 + + + 日语 + + + 韩语 + Url da solicitação de entrada com modelo composto @@ -1505,6 +1544,15 @@ 更新 + + 请输入账号 + + + Digite sua senha + + + 账号密码登录 + Leve-me até lá @@ -1676,6 +1724,9 @@ A exibição de personagens está desativada. Por favor, ative-a no jogo. + + 共 {0} 位角色 + 复制角色详情失败 @@ -1880,9 +1931,15 @@ Falha na importação + + 当前选中用户和游戏服务器不匹配,请重新选择 + 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 + + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 + Falha ao converter o servidor @@ -2555,6 +2612,15 @@ 自动化任务 + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + Geral @@ -2579,9 +2645,27 @@ Atividade no Discord + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + Arquivo + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + Comunicações entre processos @@ -2627,6 +2711,15 @@ Registro + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + Pacote OTA @@ -2672,12 +2765,27 @@ Você precisa converter para um servidor que corresponda ao inicializador antes de atualizar a versão + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2702,6 +2810,12 @@ Ativado + + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 + + + 使用 米游社 / HoYoLAB 用户登录 + Aproveite as vantagens das telas que suportam HDR para obter imagens mais brilhantes, mais vívidas e mais detalhadas @@ -2981,6 +3095,9 @@ Abrir pasta do fundo + + 转换服务器模式 + Resetar @@ -3011,6 +3128,12 @@ Ao definir o caminho do jogo, selecione o programa do jogo (Yuanshen.exe ou GenshinImpact.exe) em vez do iniciador do jogo (launcher.exe) + + 选择点击分享按钮后保存图片的模式 + + + 分享保存模式 + Experiência Shell @@ -3071,6 +3194,9 @@ Webview2 Runtime + + WebView + 下半 @@ -3113,6 +3239,9 @@ Outro + + 名片 + Ocupação @@ -3281,6 +3410,12 @@ Ferramentas oficiais + + 账密登录 + + + 三方登录 + Login na web @@ -3383,6 +3518,21 @@ Copiado para a área de transferência + + 保存分享图片 + + + 图片保存失败 + + + 图片保存成功 + + + 复制到剪贴板 + + + 保存为文件 + Todas as missões de arconte concluídas diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index aeb147c1ce..3e9c767a5d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -670,6 +670,15 @@ 单个成就存档内发现多个相同的成就 Id + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + 攻击力 Need EXACT same string in game @@ -929,6 +938,9 @@ 未找到可用的 Url + + 游戏正在运行中,关闭游戏后重试 + 提供的 Url 无效 @@ -1145,6 +1157,9 @@ 输入的 Cookie 无法获取用户信息 + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1923,7 +1938,7 @@ 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 - 使用米游社用户登录失败,请重新登录 + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 切换服务器失败 @@ -2597,12 +2612,24 @@ 自动化任务 + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + 常规 所有选项仅会在启动游戏成功后保存 + + 注入功能非常危险且有可能会造成严重后果,请谨慎使用 + 移除光照渲染中的迷雾 @@ -2621,9 +2648,27 @@ Discord Activity + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + 文件 + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + 进程联动 @@ -2669,6 +2714,15 @@ 注册表 + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + 增量包 @@ -2714,12 +2768,27 @@ 版本更新前需要提前转换至与启动器匹配的服务器 + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2745,10 +2814,10 @@ 启用 - 使用当前选中的米游社用户登录,仅支持官服 + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 - 使用米游社用户登录 + 使用 米游社 / HoYoLAB 用户登录 充分利用支持高动态范围的显示器获得更亮、更生动、更精细的画面 @@ -3029,6 +3098,9 @@ 打开背景图片文件夹 + + 转换服务器模式 + 重置 @@ -3170,6 +3242,9 @@ 其它 + + 名片 + 所属 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ru.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ru.resx index 7de85367fd..5fed99ed59 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ru.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ru.resx @@ -670,6 +670,15 @@ В одном архиве достижений обнаружено несколько одинаковых идентификаторов достижений + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + Сила атаки Need EXACT same string in game @@ -929,6 +938,9 @@ Can't find available URL + + 游戏正在运行中,关闭游戏后重试 + Указан неверный Url-адрес @@ -1145,6 +1157,9 @@ Предоставленные Cookie не позволяют получить информацию о пользователе. + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1376,6 +1391,30 @@ 手动输入祈愿记录 Url + + 游戏服务器 + + + 当前选择目录为HDD,建议使用SSD + + + 设置游戏安装目录 + + + 安装游戏 + + + 汉语 + + + 英语 + + + 日语 + + + 韩语 + 请输入请求接口的 Url 复合模板 @@ -1505,6 +1544,15 @@ 更新 + + 请输入账号 + + + Введите пароль + + + 账号密码登录 + 立即前往 @@ -1676,6 +1724,9 @@ 角色展柜尚未开启,请前往游戏操作后重试 + + 共 {0} 位角色 + 复制角色详情失败 @@ -1880,9 +1931,15 @@ Ошибка загрузки + + 当前选中用户和游戏服务器不匹配,请重新选择 + 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 + + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 + 切换服务器失败 @@ -2555,6 +2612,15 @@ Автоматизированные задачи + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + 常规 @@ -2579,9 +2645,27 @@ Активность в Discord + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + Файлы + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + 进程间 @@ -2627,6 +2711,15 @@ Реестр Игры + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + Пакет OTA @@ -2672,12 +2765,27 @@ 版本更新前需要提前转换至与启动器匹配的服务器 + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2702,6 +2810,12 @@ Включено + + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 + + + 使用 米游社 / HoYoLAB 用户登录 + Используйте преимущества мониторов с поддержкой HDR для получения более ярких, живых и детализированных изображений @@ -2981,6 +3095,9 @@ 打开背景图片文件夹 + + 转换服务器模式 + 重置 @@ -3011,6 +3128,12 @@ 设置游戏路径时,请选择游戏本体(YuanShen.exe 或 GenshinImpact.exe)而不是启动器(launcher.exe) + + 选择点击分享按钮后保存图片的模式 + + + 分享保存模式 + Shell 体验 @@ -3071,6 +3194,9 @@ Webview2 Runtime + + WebView + 下半 @@ -3113,6 +3239,9 @@ 其它 + + 名片 + 所属 @@ -3281,6 +3410,12 @@ Официальные инструменты + + 账密登录 + + + 三方登录 + Вход через браузер @@ -3383,6 +3518,21 @@ Скопировано в буфер обмена + + 保存分享图片 + + + 图片保存失败 + + + 图片保存成功 + + + 复制到剪贴板 + + + 保存为文件 + 所有魔神任务已完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.vi.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.vi.resx index bd6fbe7f2e..3443eb21bf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.vi.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.vi.resx @@ -670,6 +670,15 @@ Đã tìm thấy nhiều ID thành tựu giống hệt nhau trong kho lưu trữ thành tựu + + 【上架时间】 + + + 永久开放 + + + 〓更新时间〓 + Need EXACT same string in game @@ -929,6 +938,9 @@ 未找到可用的 Url + + 游戏正在运行中,关闭游戏后重试 + 提供的 Url 无效 @@ -1145,6 +1157,9 @@ 输入的 Cookie 无法获取用户信息 + + 获取用户信息中,请稍候... + 追加属性推荐 @@ -1376,6 +1391,30 @@ 手动输入祈愿记录 Url + + 游戏服务器 + + + 当前选择目录为HDD,建议使用SSD + + + 设置游戏安装目录 + + + 安装游戏 + + + 汉语 + + + 英语 + + + 日语 + + + 韩语 + 请输入请求接口的 Url 复合模板 @@ -1505,6 +1544,15 @@ 更新 + + 请输入账号 + + + 请输入密码 + + + 账号密码登录 + 立即前往 @@ -1676,6 +1724,9 @@ 角色展柜尚未开启,请前往游戏操作后重试 + + 共 {0} 位角色 + 复制角色详情失败 @@ -1880,9 +1931,15 @@ 导入失败 + + 当前选中用户和游戏服务器不匹配,请重新选择 + 当前选中的游戏目录 {0} 已损坏,请手动删除文件夹后重新安装游戏 + + 使用 米游社 / HoYoLAB 用户登录失败,请重新登录 + 切换服务器失败 @@ -2555,6 +2612,15 @@ 自动化任务 + + 当出现错误代码 31-4302 时请启用此选项 + + + 让我进去! + + + 游戏启动后无法再次切换 + 常规 @@ -2579,9 +2645,27 @@ Discord Activity + + 游戏文件管理/切换游戏服务器/注入功能需要管理员权限 + 文件 + + 修正角色/队伍配置/祈愿/纪行等特殊界面的视野 + + + 特殊界面修正 + + + 游戏启动后可以实时切换 + + + 将消息模块注入游戏,以便实现一些高级但危险的功能 + + + 注入 + 进程联动 @@ -2627,6 +2711,15 @@ 注册表 + + 队伍配置冷开关 + + + 移除打开队伍配置界面时显示的进度条 + + + 移除队伍配置进度条 + 增量包 @@ -2672,12 +2765,27 @@ 版本更新前需要提前转换至与启动器匹配的服务器 + + 视野冷开关 + 调整相机视野,默认 45 调整视野 + + 视野热开关 + + + 目标帧率 + + + 帧率热开关 + + + 将鼠标悬停在功能开关/文本框上查看详细说明 + 请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率 @@ -2702,6 +2810,12 @@ 启用 + + 使用当前选中的 米游社 / HoYoLAB 用户登录,需要启用「启动参数」 + + + 使用 米游社 / HoYoLAB 用户登录 + 充分利用支持高动态范围的显示器获得更亮、更生动、更精细的画面 @@ -2981,6 +3095,9 @@ 打开背景图片文件夹 + + 转换服务器模式 + 重置 @@ -3011,6 +3128,12 @@ 设置游戏路径时,请选择游戏本体(YuanShen.exe 或 GenshinImpact.exe)而不是启动器(launcher.exe) + + 选择点击分享按钮后保存图片的模式 + + + 分享保存模式 + Shell 体验 @@ -3071,6 +3194,9 @@ Webview2 运行时 + + WebView + 下半 @@ -3113,6 +3239,9 @@ 其它 + + 名片 + 所属 @@ -3281,6 +3410,12 @@ 旅行工具 + + 账密登录 + + + 三方登录 + 网页登录 @@ -3383,6 +3518,21 @@ 已复制到剪贴板 + + 保存分享图片 + + + 图片保存失败 + + + 图片保存成功 + + + 复制到剪贴板 + + + 保存为文件 + 所有魔神任务已完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx index 738acaeade..76c02d46cc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx @@ -670,6 +670,15 @@ 單個成就存檔內發現多個相同的成就 Id + + 【上架時間】 + + + 永久開放 + + + 〓更新時間〓 + 攻擊力 Need EXACT same string in game @@ -929,6 +938,9 @@ 找不到可用的 URL + + 遊戲正在執行中,關閉遊戲後重試 + 提供的 URL 無效 @@ -1145,6 +1157,9 @@ 輸入的 Cookie 無法獲取使用者資訊 + + 獲取使用者資訊中,請稍候... + 追加屬性推薦 @@ -1376,6 +1391,30 @@ 手動輸入祈願紀錄 URL + + 遊戲伺服器 + + + 目前選擇目錄為HDD,建議使用SSD + + + 設定遊戲安裝目錄 + + + 安裝遊戲 + + + 漢語 + + + 英語 + + + 日語 + + + 韓語 + 請輸入請求介面的 URL 複合模板 @@ -1505,6 +1544,15 @@ 更新 + + 請輸入帳號 + + + 請輸入密碼 + + + 帳號密碼登入 + 立即前往 @@ -1676,6 +1724,9 @@ 角色展櫃尚未開啟,請前往遊戲操作後重試 + + 共 {0} 位角色 + 複製角色詳情失敗 @@ -1695,7 +1746,7 @@ 操作未全部完成:添加/更新:{0} 個,跳過 {1} 個 - 選定的等級不需要養成材料 + 選擇的等級不需要養成材料 尚未創建並選擇養成計劃 @@ -1869,7 +1920,7 @@ 請輸入正確的電子郵件地址 - 剪貼簿中的文本格式不正確 + 剪貼簿中的文字格式不正確 數據格式不正確 @@ -1880,9 +1931,15 @@ 匯入失敗 + + 目前選中使用者與遊戲伺服器不匹配,請重新選擇 + 目前選中的遊戲目錄 {0} 已損壞,請手動刪除資料夾後重新安裝遊戲 + + 使用 米遊社 / HoYoLAB 使用者登入失敗,請重新登入 + 切換伺服器失敗 @@ -2073,7 +2130,7 @@ 圖像輸出 - 匯出文本到剪貼簿 + 匯出文字到剪貼簿 角色屬性 @@ -2163,10 +2220,10 @@ 新增任意武器到養成計劃 - 新增即時便籤 + 新增即時便箋 - 新增即時便籤以定時重新整理 + 新增即時便箋以定時重新整理 新增 @@ -2214,7 +2271,7 @@ 自動重新整理 - 間隔選定的時間後重新整理新增的即時便箋 + 間隔選擇的時間後重新整理新增的即時便箋 這些選項僅允許在非系統管理員模式下更改 @@ -2555,6 +2612,15 @@ 自動化任務 + + 當出現錯誤代碼 31-4302 時請啟用此選項 + + + 讓我進去! + + + 遊戲啟動後無法再次切換 + 一般 @@ -2579,9 +2645,27 @@ Discord Activity + + 遊戲檔案管理/切換遊戲伺服器/注入功能需要系統管理員權限 + 檔案 + + 修正角色/隊伍設定/祈願/紀行等特殊界面的視野 + + + 特殊界面修正 + + + 遊戲啟動後可以即時切換 + + + 將消息模組注入遊戲,以便實現一些高級但危險的功能 + + + 注入 + 處理程序聯動 @@ -2627,6 +2711,15 @@ 登錄檔 + + 隊伍設定冷開關 + + + 移除打開隊伍設定界面時顯示的進度條 + + + 移除隊伍設定進度條 + 附加包 @@ -2672,12 +2765,27 @@ 版本更新前需要提前轉換至與啟動器對應的伺服器 + + 視野冷開關 + 調整相機視野,預設 45 調整視野 + + 視野熱開關 + + + 目標 FPS + + + FPS 熱開關 + + + 將滑鼠懸停在功能開關/文字框上查看詳細說明 + 請在遊戲內關閉「垂直同步」選項,需要高性能的顯示卡以支援更高的 FPS ,將值設定為 -1 以表示無限制 FPS @@ -2702,6 +2810,12 @@ 啟用 + + 使用目前選中的 米遊社 / HoYoLAB 使用者登入,需要啟用「啟動參數」 + + + 使用 米遊社 / HoYoLAB 使用者登入 + 充分利用支援 HDR 的顯示器以獲得更亮、更生動、更精細的畫面 @@ -2724,7 +2838,7 @@ 開啟截圖資料夾 - 選定遊戲路徑 + 選擇遊戲路徑 關於 胡桃 @@ -2889,7 +3003,7 @@ 日曆 - 即時便籤 + 即時便箋 祈願紀錄 @@ -2981,6 +3095,9 @@ 打開背景圖片資料夾 + + 轉換伺服器模式 + 重設 @@ -3011,6 +3128,12 @@ 設定遊戲路徑時,請選擇遊戲本體(YuanShen.exe 或 GenshinImpact.exe) 而不是啟動器(launcher.exe) + + 選擇點擊分享按鈕後儲存圖片的模式 + + + 分享儲存模式 + Shell 體驗 @@ -3071,6 +3194,9 @@ WebView2 執行階段 + + WebView + 下半 @@ -3113,6 +3239,9 @@ 其他 + + 名片 + 所屬 @@ -3281,6 +3410,12 @@ 旅行工具 + + 帳密登入 + + + 第三方登入 + 網頁登入 @@ -3383,6 +3518,21 @@ 已複製到剪貼簿 + + 儲存分享圖片 + + + 圖片儲存失敗 + + + 圖片儲存成功 + + + 複製到剪貼簿 + + + 儲存為檔案 + 所有魔神任務已完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.en.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.en.resx index 305dfab1ea..6ae45b3d66 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.en.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.en.resx @@ -101,25 +101,16 @@ ^Local Specialty (\([a-zA-Z]+\))$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + Persons with time of activity|Persons who have tasks open time|Persons who wish to introduce participants|Persons who earn reward time|[Hold time] - - Persons with active time.*?(?:(\d\.\d) continue to open during version |(\d\.\d) after version update.*? to \d\.\d finish) - - - (?:Active time union|prayertime|= time for discounting).*?(\d\.\d) updated version .*?~.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + Will update maintenance on&lt;t class="t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;Version maintenance - + \d\.\d update maintenance advance - - 〓Update Maintenance Duration〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + Version \d\.\d Update Details \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.fr.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.fr.resx index 6dd651dca7..29cb554068 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.fr.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.fr.resx @@ -101,25 +101,16 @@ ^Produit de [a-zA-Z]+$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓更新时间〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + Détails de la mise à jour \d\.\d \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.id.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.id.resx index b29379618c..458fba7bfb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.id.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.id.resx @@ -101,25 +101,16 @@ ^Produk khas [a-zA-Z]+$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓Durasi Pemeliharaan Pembaruan.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + Versi \d\.\d Detail Pembaruan \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ja.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ja.resx index 3536505b51..328218076c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ja.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ja.resx @@ -101,25 +101,16 @@ ^[\u3000-\u9fa5]{2,6}地域の特産$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓イベント期間〓|〓任務開放時間〓|〓祈願詳細〓|〓割引期間〓|〓報酬獲得期限〓|【リリース時間】 - - 更新時刻は発せら。*?(\d\.\d) で終了します。(\d\.\d) 再度 (\d\.\d.\d.\dバージョン終了) + + &lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;にてバージョンアップのメンテナンスが行われる予定です - - (?:アクティビティ種別|販売" |通信種別" |販売種別" のみ).*?(\d\.\.\d)バージョン更新後.*?~.*?&lt;t class="t_(?gl|lc)".*?&gt;(.*?*?)&lt;/t&gt; + + Ver.\d\.\dバージョンアップメンテナンス予告 - - &lt;t class=\"t_(?gl|lc)\".*?&gt;(.*?)&lt;/t&gt;メンテナンス版のためアップグレードしてください。 - - - \d\.\dバージョンによるメンテナンスの予約 - - - 〓メンテナンス時間〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - - Ver.\d\.\d.+正式リリース + + Ver.\d\.\dバージョンアップのお知らせ \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ko.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ko.resx index d31e7ec5e5..a08c1dbff8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ko.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ko.resx @@ -101,25 +101,16 @@ ^[\u0000-\uffff]{2} 지역 특산물$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓更新时间〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + \d\.\d版本更新说明 \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.pt.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.pt.resx index 655eb89bc4..d1b220ca2d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.pt.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.pt.resx @@ -101,25 +101,16 @@ ^Especialidade de [a-zA-Z]+$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓Duração da manutenção da atualização.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + Versão \d\.\d Detalhes da atualização \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx index 5146b2bc12..5f06af7e80 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx @@ -101,25 +101,16 @@ ^[\u4e00-\u9fa5]{2}区域特产$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(?:(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; *?))后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓更新时间〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + \d\.\d版本更新说明 \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ru.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ru.resx index 0911fa9bbc..e5ece37fd1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ru.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.ru.resx @@ -101,25 +101,16 @@ ^Диковинка [\u0000-\uffff]+$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓更新时间〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + Версия \d\.\d Подробности обновления \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.vi.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.vi.resx index 66368fb48d..7414260b12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.vi.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.vi.resx @@ -101,25 +101,16 @@ ^Đặc Sản Khu Vực [a-zA-z]+$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;)后永久开放 + + 〓活动时间〓|〓任务开放时间〓|〓祈愿介绍〓|〓折扣时间〓|〓获取奖励时限〓|【上架时间】 - - 〓活动时间〓.*?(?:(\d\.\d)版本期间持续开放|(\d\.\d)版本更新后.*?至\d\.\d版本结束) - - - (?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 将于&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;进行版本更新维护 - + \d\.\d版本更新维护预告 - - 〓更新时间〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; - - + \d\.\d版本更新说明 \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.zh-Hant.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.zh-Hant.resx index b85ee8d679..e719dfa6d9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.zh-Hant.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.zh-Hant.resx @@ -101,25 +101,16 @@ ^[\u4e00-\u9fa5]{2}區域特產$ If you don't know what is regex, DO NOT TRANSLATE THIS! - - (?:(?:〓活動時間〓|〓任務開放時間〓).*?(?:(\d\.\d)版本更新(?:完成|)|&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; *?))後永久開放 + + 〓活動時間〓|〓任務開放時間〓|〓祈願介紹〓|〓折扣時間〓|〓獲取獎勵時限〓|【上架時間】 - - 〓活動時間〓.*?(?:(\d\.\d)版本期間持續開放|(\d\.\d)版本更新後.*?至\d\.\d版本結束) - - - (?:〓活動時間〓|祈願時間|【上架時間】|〓折扣時間〓).*?(\d\.\d)版本更新後.*?~.*?&lt;t class="t_(?:gl|lc)".*?&gt;(.*?)&lt;/t&gt; - - + 將於&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;進行版本更新維護 - - \d\.\d版本更新維護預告 - - - 〓更新時間〓.+?&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt; + + \d\.\d版本更新停服說明 - + \d\.\d版本更新說明 \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/ThirdParty/hoyolab_ic_launcher_foreground.webp b/src/Snap.Hutao/Snap.Hutao/Resource/ThirdParty/hoyolab_ic_launcher_foreground.webp new file mode 100644 index 0000000000..2473f15fce Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/ThirdParty/hoyolab_ic_launcher_foreground.webp differ diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/ThirdParty/miyoushe_ic_launcher.png b/src/Snap.Hutao/Snap.Hutao/Resource/ThirdParty/miyoushe_ic_launcher.png new file mode 100644 index 0000000000..43f4945e96 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/ThirdParty/miyoushe_ic_launcher.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/RepositoryExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/RepositoryExtension.cs index 0b3fd2c60f..6b9589b2d7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/RepositoryExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Abstraction/RepositoryExtension.cs @@ -63,6 +63,12 @@ public static TEntity Single(this IRepository repository, Expr return repository.Query(query => query.Single(predicate)); } + public static TResult Single(this IRepository repository, Func, IQueryable> query) + where TEntity : class + { + return repository.Query(query1 => query(query1).Single()); + } + public static TEntity? SingleOrDefault(this IRepository repository, Expression> predicate) where TEntity : class { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementHtmlVisitor.cs b/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementHtmlVisitor.cs index eadcd6a00f..8efe16be9d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementHtmlVisitor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementHtmlVisitor.cs @@ -4,72 +4,43 @@ using AngleSharp; using AngleSharp.Dom; using AngleSharp.Html.Dom; -using System.Text; using System.Text.RegularExpressions; namespace Snap.Hutao.Service.Announcement; internal static partial class AnnouncementHtmlVisitor { - public static async ValueTask VisitActivityAsync(IBrowsingContext context, string content) + public static async ValueTask> VisitActivityAsync(IBrowsingContext context, string content) { IDocument document = await context.OpenAsync(rsp => rsp.Content(content)).ConfigureAwait(false); IHtmlElement? body = document.Body; ArgumentNullException.ThrowIfNull(body); - foreach (IElement element in body.Children) + return body.Children + .Where(e => e is IHtmlParagraphElement) + .Where(e => AnnouncementRegex.ValidDescriptionsRegex.IsMatch(e.TextContent)) + .Select(e => ParseElementToTimeStrings((IHtmlParagraphElement)e)) + .MaxBy(r => r.Count) ?? []; + + List ParseElementToTimeStrings(IHtmlParagraphElement paragraph) { - if (element is not IHtmlParagraphElement paragraph) - { - continue; - } + string textContent = paragraph.TextContent.Trim(); - if (paragraph.TextContent is not ("〓活动时间〓" or "〓祈愿介绍〓" or "〓任务开放时间〓" or "〓折扣时间〓" or "〓重置时间〓")) + // All in span, special case + if (textContent.Contains(SH.ServiceAnnouncementAdventurersBoosterBundlesDurationDescription, StringComparison.CurrentCulture)) { - continue; + return TimeOrVersionRegex().Matches(textContent).Select(r => r.Value).ToList(); } - if (paragraph.NextElementSibling is IHtmlParagraphElement { Children: [IHtmlSpanElement, ..] } nextParagraph) + if (paragraph.NextElementSibling is null) { - return nextParagraph.TextContent; + return []; } - if (paragraph.NextElementSibling is IHtmlDivElement div) - { - foreach (IElement element2 in div.Children) - { - if (element2 is not IHtmlTableElement table) - { - continue; - } - - IHtmlTableRowElement header = table.Rows[0]; - StringBuilder timeBuilder = new(); - int actualIndex = -1; - foreach (IHtmlTableCellElement cell in header.Cells) - { - actualIndex += cell.ColumnSpan; - if (cell.TextContent is "开启时间") - { - timeBuilder.Append(table.Rows[1].Cells[actualIndex].TextContent).Append(" ~ "); - } - - if (cell.TextContent is "结束时间") - { - timeBuilder.Append(table.Rows[1].Cells[actualIndex].TextContent); - return timeBuilder.ToString(); - } + string nextTextContent = paragraph.NextElementSibling.TextContent.Trim(); - if (cell.TextContent is "祈愿时间") - { - return table.Rows[1].Cells[actualIndex].TextContent; - } - } - } - } + return TimeOrVersionRegex().Matches(nextTextContent).Select(r => r.Value).ToList(); } - - return string.Empty; } public static async ValueTask VisitAnnouncementAsync(IBrowsingContext context, string content) @@ -85,20 +56,20 @@ public static async ValueTask VisitAnnouncementAsync(IBrowsingContext co continue; } - if (paragraph.TextContent is not "〓更新时间〓") + if (!paragraph.TextContent.Equals(SH.ServiceAnnouncementVersionUpdateTimeDescription, StringComparison.CurrentCulture)) { continue; } - if (paragraph.NextElementSibling is IHtmlParagraphElement { Children: [IHtmlSpanElement, ..] } nextParagraph) + if (paragraph.NextElementSibling is IHtmlParagraphElement nextParagraph) { - return TimeRegex().Match(nextParagraph.TextContent).Value; + return TimeOrVersionRegex().Match(nextParagraph.TextContent).Value; } } return string.Empty; } - [GeneratedRegex(@"\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}")] - private static partial Regex TimeRegex(); + [GeneratedRegex(@".*?\d\.\d.*?[~-]|.*?\d\.\d.*?$|\d{4}/\d{2}/\d{2} \d{2}:\d{2}(?::\d{2})?")] + private static partial Regex TimeOrVersionRegex(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementRegex.cs b/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementRegex.cs new file mode 100644 index 0000000000..132b39b3f8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementRegex.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.RegularExpressions; + +namespace Snap.Hutao.Service.Announcement; + +internal static partial class AnnouncementRegex +{ + /// + public static readonly Regex ValidDescriptionsRegex = new(SHRegex.ServiceAnnouncementMatchValidDescriptions, RegexOptions.Compiled); + + /// + public static readonly Regex VersionUpdateTitleRegex = new(SHRegex.ServiceAnnouncementMatchVersionUpdateTitle, RegexOptions.Compiled); + + /// + public static readonly Regex VersionUpdatePreviewTitleRegex = new(SHRegex.ServiceAnnouncementMatchVersionUpdatePreviewTitle, RegexOptions.Compiled); + + /// + public static readonly Regex VersionUpdatePreviewTimeRegex = new(SHRegex.ServiceAnnouncementMatchVersionUpdatePreviewTime, RegexOptions.Compiled); + + [GeneratedRegex("<t class=\"t_(?:gl|lc)\".*?>(?:)?(.*?)(?:)?</t>", RegexOptions.Multiline)] + public static partial Regex XmlTimeTagRegex(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementService.cs index ad4414cb39..fd65b176a9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Announcement/AnnouncementService.cs @@ -123,6 +123,10 @@ private async ValueTask AdjustAnnouncementTimeAsync(List DTO Dictionary versionStartTimes = []; + // Workaround for some long-term activities + versionStartTimes.TryAdd("5.0", UnsafeDateTimeOffset.ParseDateTime("2024/08/28 06:00".AsSpan(), offset)); + versionStartTimes.TryAdd("5.1", UnsafeDateTimeOffset.ParseDateTime("2024/10/09 06:00".AsSpan(), offset)); + IBrowsingContext context = BrowsingContext.New(Configuration.Default); // 更新公告 @@ -147,82 +151,44 @@ private async ValueTask AdjustAnnouncementTimeAsync(List times = await AnnouncementHtmlVisitor.VisitActivityAsync(context, announcement.Content).ConfigureAwait(false); + logger.LogInformation("{Title} '{Time}'", announcement.Subtitle, string.Join(",", times)); - if (versionStartTimes.TryGetValue(persistent.Groups[2].Value, out versionStartTime)) - { - announcement.StartTime = versionStartTime; - announcement.EndTime = versionStartTime + TimeSpan.FromDays(42); - continue; - } - } - - if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient) + if (times.Count is 0) { - if (versionStartTimes.TryGetValue(transient.Groups[1].Value, out versionStartTime)) - { - announcement.StartTime = versionStartTime; - announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset); - continue; - } + continue; } - MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex().Matches(announcement.Content); - if (matches.Count < 2) + if (times.Count is 1 && times[0].Contains(SH.ServiceAnnouncementPermanentKeyword, StringComparison.InvariantCulture)) { + announcement.StartTime = ParseTime(times[0]); continue; } - List dateTimes = []; - foreach (Match timeMatch in (IList)matches) + List timeOffsets = times.Select(ParseTime).ToList().SortBy(dto => dto); + + DateTimeOffset startTime = timeOffsets.First(); + DateTimeOffset endTime = timeOffsets.Last(); + if (startTime == endTime) { - dateTimes.Add(UnsafeDateTimeOffset.ParseDateTime(timeMatch.Groups[1].ValueSpan, offset)); + endTime += TimeSpan.FromDays(42); } - DateTimeOffset min = DateTimeOffset.MaxValue; - DateTimeOffset max = DateTimeOffset.MinValue; + announcement.StartTime = startTime; + announcement.EndTime = endTime; - foreach (DateTimeOffset time in dateTimes) + DateTimeOffset ParseTime(string text) { - if (time < min) + if (VersionRegex().Match(text) is { Success: true } version) { - min = time; + if (versionStartTimes.TryGetValue(version.Groups[1].Value, out DateTimeOffset versionStartTime)) + { + return versionStartTime; + } } - if (time > max) - { - max = time; - } + return UnsafeDateTimeOffset.ParseDateTime(text, offset); } - - announcement.StartTime = min; - announcement.EndTime = max; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index e16b17f0ac..a75195341a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -6,6 +6,7 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.BackgroundImage; +using Snap.Hutao.Service.Game.Package; using Snap.Hutao.UI.Xaml.Media.Backdrop; using Snap.Hutao.Web.Bridge; using Snap.Hutao.Web.Hoyolab; @@ -25,6 +26,7 @@ internal sealed partial class AppOptions : DbStoreOptions private Region? region; private string? geetestCustomCompositeUrl; private int? downloadSpeedLimitPerSecondInKiloByte; + private PackageConverterType? packageConverterType; private BridgeShareSaveType? bridgeShareSaveType; public bool IsNotifyIconEnabled @@ -94,6 +96,14 @@ public int DownloadSpeedLimitPerSecondInKiloByte set => SetOption(ref downloadSpeedLimitPerSecondInKiloByte, SettingEntry.DownloadSpeedLimitPerSecondInKiloByte, value); } + public List> PackageConverterTypes { get; } = CollectionsNameValue.FromEnum(); + + public PackageConverterType PackageConverterType + { + get => GetOption(ref packageConverterType, SettingEntry.PackageConverterType, EnumParse, PackageConverterType.ScatteredFiles).Value; + set => SetOption(ref packageConverterType, SettingEntry.PackageConverterType, value, EnumToStringOrEmpty); + } + public List> BridgeShareSaveTypes { get; } = CollectionsNameValue.FromEnum(type => type.GetLocalizedDescription()); public BridgeShareSaveType BridgeShareSaveType diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs index b8922e5ae3..8b47ddd28d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs @@ -48,7 +48,7 @@ public unsafe AvatarView Create() .SetId(avatar.Id) .SetName(avatar.Name) .SetQuality(avatar.Quality) - .SetNameCard(AvatarNameCardPicConverter.AvatarToUri(avatar)) + .SetNameCard(AvatarNameCardPicConverter.IconNameToUri(avatar.NameCard.PicturePrefix)) .SetElement(ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore)) .SetConstellations(avatar.SkillDepot.Talents, activatedConstellations) .SetSkills(avatar.SkillDepot.CompositeSkillsNoInherents(), character.Skills.ToDictionary(s => s.SkillId, s => s.Level), extraLevels) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/BackgroundImage/BackgroundImageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/BackgroundImage/BackgroundImageService.cs index 2ef3c40745..7ba7d21f43 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/BackgroundImage/BackgroundImageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/BackgroundImage/BackgroundImageService.cs @@ -10,6 +10,7 @@ using Snap.Hutao.Web.Response; using Snap.Hutao.Win32.Foundation; using System.Collections.Frozen; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Windows.Graphics.Imaging; @@ -27,25 +28,32 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService private readonly ITaskContext taskContext; private readonly AppOptions appOptions; - private HashSet? currentBackgroundPathSet; + private HashSet? availableBackgroundPathSet; public async ValueTask> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default) { - HashSet backgroundSet = await SkipOrInitBackgroundAsync(token).ConfigureAwait(false); + // The availableBackgroundSet will be empty if BackgroundImageType is None + // backgroundImageOptions.Wallpaper will also be set in this method if web wallpaper type is selected + HashSet availableBackgroundSet = await SkipOrInitAvailableBackgroundAsync(token).ConfigureAwait(false); - if (backgroundSet.Count <= 0) + if (availableBackgroundSet.Count <= 0) { return new(true, default!); } - string path = System.Random.Shared.GetItems([.. backgroundSet], 1)[0]; - backgroundSet.Remove(path); + string path = System.Random.Shared.GetItems([.. availableBackgroundSet], 1)[0]; + availableBackgroundSet.Remove(path); if (string.Equals(path, previous?.Path, StringComparison.OrdinalIgnoreCase)) { return new(false, default!); } + if (!File.Exists(path)) + { + Debugger.Break(); + } + using (FileStream fileStream = File.OpenRead(path)) { BitmapDecoder decoder; @@ -80,17 +88,17 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService } } - private async ValueTask> SkipOrInitBackgroundAsync(CancellationToken token = default) + private async ValueTask> SkipOrInitAvailableBackgroundAsync(CancellationToken token = default) { switch (appOptions.BackgroundImageType) { case BackgroundImageType.LocalFolder: { - if (currentBackgroundPathSet is not { Count: > 0 }) + if (availableBackgroundPathSet is not { Count: > 0 }) { string backgroundFolder = HutaoRuntime.GetDataFolderBackgroundFolder(); - currentBackgroundPathSet = Directory + availableBackgroundPathSet = Directory .EnumerateFiles(backgroundFolder, "*", SearchOption.AllDirectories) .Where(path => AllowedFormats.Contains(Path.GetExtension(path))) .ToHashSet(); @@ -101,20 +109,20 @@ private async ValueTask> SkipOrInitBackgroundAsync(CancellationT } case BackgroundImageType.HutaoBing: - await SetCurrentBackgroundPathSetAsync((client, token) => client.GetBingWallpaperAsync(token), token).ConfigureAwait(false); + await SetCurrentBackgroundPathSetAsync(static (client, token) => client.GetBingWallpaperAsync(token), token).ConfigureAwait(false); break; case BackgroundImageType.HutaoDaily: - await SetCurrentBackgroundPathSetAsync((client, token) => client.GetTodayWallpaperAsync(token), token).ConfigureAwait(false); + await SetCurrentBackgroundPathSetAsync(static (client, token) => client.GetTodayWallpaperAsync(token), token).ConfigureAwait(false); break; case BackgroundImageType.HutaoOfficialLauncher: - await SetCurrentBackgroundPathSetAsync((client, token) => client.GetLauncherWallpaperAsync(token), token).ConfigureAwait(false); + await SetCurrentBackgroundPathSetAsync(static (client, token) => client.GetLauncherWallpaperAsync(token), token).ConfigureAwait(false); break; default: - currentBackgroundPathSet = []; + availableBackgroundPathSet = []; break; } - return currentBackgroundPathSet ??= []; + return availableBackgroundPathSet ??= []; async Task SetCurrentBackgroundPathSetAsync(Func>> responseFactory, CancellationToken token = default) { @@ -125,7 +133,12 @@ async Task SetCurrentBackgroundPathSetAsync(Func().GetFileFromCacheAsync(url).ConfigureAwait(false); - currentBackgroundPathSet = [file]; + if (!File.Exists(file)) + { + Debugger.Break(); + } + + availableBackgroundPathSet = [file]; } await taskContext.SwitchToMainThreadAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs index 3202149cf0..4f82b46c4c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs @@ -41,12 +41,6 @@ public void UpdateCultivateItem(CultivateItem item) this.Update(item); } - [Obsolete] - public CultivateEntry? GetCultivateEntryByProjectIdAndItemId(Guid projectId, uint itemId) - { - return this.SingleOrDefault(e => e.ProjectId == projectId && e.Id == itemId); - } - public List GetCultivateEntryListByProjectIdAndItemId(Guid projectId, uint itemId) { return this.List(e => e.ProjectId == projectId && e.Id == itemId); @@ -96,4 +90,9 @@ public void AddLevelInformation(CultivateEntryLevelInformation levelInformation) { return this.SingleOrDefault(p => p.InnerId == projectId); } + + public Guid GetCultivateProjectIdByEntryId(Guid entryId) + { + return this.Single(query => query.Where(entry => entry.InnerId == entryId).Select(entry => entry.InnerId)); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index ff2c22cbd2..b988e63099 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -8,6 +8,7 @@ using Snap.Hutao.Service.Inventory; using Snap.Hutao.Service.Metadata.ContextAbstraction; using Snap.Hutao.ViewModel.Cultivation; +using System.Collections.Concurrent; using System.Collections.ObjectModel; using ModelItem = Snap.Hutao.Model.Item; @@ -17,6 +18,9 @@ namespace Snap.Hutao.Service.Cultivation; [Injection(InjectAs.Singleton, typeof(ICultivationService))] internal sealed partial class CultivationService : ICultivationService { + private readonly ConcurrentDictionary> entryCollectionCache = []; + private readonly AsyncLock entryCollectionLock = new(); + private readonly ICultivationRepository cultivationRepository; private readonly IInventoryRepository inventoryRepository; private readonly IServiceProvider serviceProvider; @@ -29,38 +33,44 @@ public AdvancedDbCollectionView Projects get => projects ??= new(cultivationRepository.GetCultivateProjectCollection(), serviceProvider); } - public ITaskContext TaskContext { get => taskContext; } - - public ICultivationRepository Repository { get => cultivationRepository; } - - public async ValueTask> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context) + public async ValueTask> GetCultivateEntryCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context) { - await taskContext.SwitchToBackgroundAsync(); - List entries = cultivationRepository.GetCultivateEntryListIncludingLevelInformationByProjectId(cultivateProject.InnerId); - - List resultEntries = new(entries.Count); - foreach (CultivateEntry entry in entries) + using (await entryCollectionLock.LockAsync().ConfigureAwait(false)) { - List entryItems = []; - - foreach (CultivateItem cultivateItem in cultivationRepository.GetCultivateItemListByEntryId(entry.InnerId)) + if (entryCollectionCache.TryGetValue(cultivateProject.InnerId, out ObservableCollection? collection)) { - entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId))); + return collection; } - ModelItem item = entry.Type switch + await taskContext.SwitchToBackgroundAsync(); + List entries = cultivationRepository.GetCultivateEntryListIncludingLevelInformationByProjectId(cultivateProject.InnerId); + + List resultEntries = new(entries.Count); + foreach (CultivateEntry entry in entries) { - CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(), - CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(), + List entryItems = []; - // TODO: support furniture calc - _ => default!, - }; + foreach (CultivateItem cultivateItem in cultivationRepository.GetCultivateItemListByEntryId(entry.InnerId)) + { + entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId))); + } - resultEntries.Add(new(entry, item, entryItems)); - } + ModelItem item = entry.Type switch + { + CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(), + CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(), + + // TODO: support furniture calc + _ => default!, + }; - return resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection(); + resultEntries.Add(new(entry, item, entryItems)); + } + + ObservableCollection result = resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection(); + entryCollectionCache.TryAdd(cultivateProject.InnerId, result); + return result; + } } public async ValueTask> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token) @@ -103,12 +113,18 @@ public async ValueTask> GetStatist public async ValueTask RemoveCultivateEntryAsync(Guid entryId) { + // Invalidate cache + entryCollectionCache.TryRemove(cultivationRepository.GetCultivateProjectIdByEntryId(entryId), out _); + await taskContext.SwitchToBackgroundAsync(); cultivationRepository.RemoveCultivateEntryById(entryId); } public void SaveCultivateItem(CultivateItemView item) { + // Invalidate cache + entryCollectionCache.TryRemove(cultivationRepository.GetCultivateProjectIdByEntryId(item.Entity.EntryId), out _); + cultivationRepository.UpdateCultivateItem(item.Entity); } @@ -119,17 +135,13 @@ public async ValueTask SaveConsumptionAsync(InputCons return ConsumptionSaveResultKind.NoItem; } - // Try select project if not selected - if (Projects.CurrentItem is null) + if (!await EnsureCurrentProjectAsync().ConfigureAwait(false)) { - await taskContext.SwitchToMainThreadAsync(); - Projects.MoveCurrentTo(Projects.SourceCollection.SelectedOrDefault()); - if (Projects.CurrentItem is null) - { - return ConsumptionSaveResultKind.NoProject; - } + return ConsumptionSaveResultKind.NoProject; } + ArgumentNullException.ThrowIfNull(Projects.CurrentItem); + await taskContext.SwitchToBackgroundAsync(); if (inputConsumption.Strategy is not ConsumptionSaveStrategyKind.CreateNewEntry) @@ -170,6 +182,9 @@ public async ValueTask SaveConsumptionAsync(InputCons IEnumerable toAdd = inputConsumption.Items.Select(item => CultivateItem.From(entry.InnerId, item)); cultivationRepository.AddCultivateItemRange(toAdd); + + // Invalidate cache + entryCollectionCache.TryRemove(Projects.CurrentItem.InnerId, out _); } return ConsumptionSaveResultKind.Added; @@ -206,8 +221,26 @@ public async ValueTask RemoveProjectAsync(CultivateProject project) await taskContext.SwitchToMainThreadAsync(); projects.Remove(project); + // Invalidate cache + entryCollectionCache.TryRemove(project.InnerId, out _); + // Sync database await taskContext.SwitchToBackgroundAsync(); cultivationRepository.RemoveCultivateProjectById(project.InnerId); } + + public async ValueTask EnsureCurrentProjectAsync() + { + if (Projects.CurrentItem is null) + { + await taskContext.SwitchToMainThreadAsync(); + Projects.MoveCurrentTo(Projects.SourceCollection.SelectedOrDefault()); + if (Projects.CurrentItem is null) + { + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationServiceExtension.cs new file mode 100644 index 0000000000..531270c765 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationServiceExtension.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Database; +using Snap.Hutao.ViewModel.Cultivation; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Cultivation; + +internal static class CultivationServiceExtension +{ + public static async ValueTask?> GetCultivateEntryCollectionForCurrentProjectAsync(this ICultivationService cultivationService, ICultivationMetadataContext context) + { + if (!await cultivationService.EnsureCurrentProjectAsync().ConfigureAwait(false)) + { + return default; + } + + ArgumentNullException.ThrowIfNull(cultivationService.Projects.CurrentItem); + return await cultivationService.GetCultivateEntryCollectionAsync(cultivationService.Projects.CurrentItem, context).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs index 9fa2a396a2..8121a55466 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs @@ -20,9 +20,6 @@ internal interface ICultivationRepository : IRepository GetCultivateEntryListByProjectId(Guid projectId); List GetCultivateItemListByEntryId(Guid entryId); @@ -44,4 +41,6 @@ internal interface ICultivationRepository : IRepository GetCultivateEntryListIncludingLevelInformationByProjectId(Guid projectId); List GetCultivateEntryListByProjectIdAndItemId(Guid projectId, uint itemId); + + Guid GetCultivateProjectIdByEntryId(Guid entryId); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index b789261d88..f6b4e4a976 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -13,11 +13,9 @@ internal interface ICultivationService { AdvancedDbCollectionView Projects { get; } - ITaskContext TaskContext { get; } + ValueTask EnsureCurrentProjectAsync(); - ICultivationRepository Repository { get; } - - ValueTask> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context); + ValueTask> GetCultivateEntryCollectionAsync(CultivateProject cultivateProject, ICultivationMetadataContext context); ValueTask> GetStatisticsCultivateItemCollectionAsync( CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Feature/FeatureService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Feature/FeatureService.cs index aad9318a08..9347618741 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Feature/FeatureService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Feature/FeatureService.cs @@ -24,15 +24,8 @@ internal sealed partial class FeatureService : IFeatureService IHttpClientFactory httpClientFactory = scope.ServiceProvider.GetRequiredService(); using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(FeatureService))) { - try - { - string url = hutaoEndpointsFactory.Create().Feature($"UnlockerIsland_{tag}"); - return await httpClient.GetFromJsonAsync(url).ConfigureAwait(false); - } - catch - { - return default; - } + string url = hutaoEndpointsFactory.Create().Feature($"UnlockerIsland_{tag}"); + return await httpClient.GetFromJsonAsync(url).ConfigureAwait(false); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs index 7196735066..e2a04b3f22 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceMetadataContext.cs @@ -42,12 +42,12 @@ public Item GetItemByNameAndType(string name, string type) { if (type == SH.ModelInterchangeUIGFItemTypeAvatar) { - result = NameAvatarMap[name].ToItem(); + result = NameAvatarMap[name].ToItem(); } if (type == SH.ModelInterchangeUIGFItemTypeWeapon) { - result = NameWeaponMap[name].ToItem(); + result = NameWeaponMap[name].ToItem(); } ArgumentNullException.ThrowIfNull(result); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs index 0b7b1192d3..f496740159 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -45,7 +45,6 @@ public static string GetCacheFile(string path) return string.Empty; } - /// public async ValueTask> GetQueryAsync() { (bool isOk, string path) = await gameService.GetGamePathAsync().ConfigureAwait(false); @@ -61,7 +60,9 @@ public async ValueTask> GetQueryAsync() return new(false, GachaLogQuery.Invalid(SH.FormatServiceGachaLogUrlProviderCachePathNotFound(cacheFile))); } - using (FileStream fileStream = File.OpenRead(GetCacheFile(path))) + // Must copy the file to avoid the following exception: + // System.IO.IOException: The process cannot access the file + using (TempFileStream fileStream = TempFileStream.CopyFrom(cacheFile, FileMode.Open, FileAccess.Read)) { using (MemoryStream memoryStream = await memoryStreamFactory.GetStreamAsync(fileStream).ConfigureAwait(false)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs index 366b7fd06d..8acebc7e12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs @@ -89,15 +89,6 @@ public bool SetGameAccount(GameAccount account) return RegistryInterop.Set(account); } - public async ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid) - { - await taskContext.SwitchToMainThreadAsync(); - gameAccount.UpdateAttachUid(uid); - - await taskContext.SwitchToBackgroundAsync(); - gameRepository.UpdateGameAccount(gameAccount); - } - public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) { LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs index 6a7c3d99c4..9c8e462eb1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs @@ -9,8 +9,6 @@ namespace Snap.Hutao.Service.Game.Account; internal interface IGameAccountService { - ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid); - GameAccount? DetectCurrentGameAccount(SchemeType schemeType); ValueTask DetectGameAccountAsync(SchemeType schemeType); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs index d604aeeb48..69416c8cbc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -44,11 +44,6 @@ public ChannelOptions GetChannelOptions() return gameAccountService.DetectCurrentGameAccount(scheme); } - public ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid) - { - return gameAccountService.AttachGameAccountToUidAsync(gameAccount, uid); - } - public ValueTask ModifyGameAccountAsync(GameAccount gameAccount) { return gameAccountService.ModifyGameAccountAsync(gameAccount); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs index 707a6c9879..354d637c76 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs @@ -10,8 +10,6 @@ namespace Snap.Hutao.Service.Game; internal interface IGameServiceFacade { - ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid); - ValueTask DetectGameAccountAsync(SchemeType scheme); ValueTask> GetGamePathAsync(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 6cd2396f30..0d1d7afe5d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -1,11 +1,14 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.Game.Launching; +using Snap.Hutao.Service.Game.Launching.Handler; using Snap.Hutao.Service.Game.PathAbstraction; using Snap.Hutao.Win32.Graphics.Gdi; using System.Collections.Immutable; @@ -17,17 +20,19 @@ namespace Snap.Hutao.Service.Game; [Injection(InjectAs.Singleton)] -internal sealed partial class LaunchOptions : DbStoreOptions +internal sealed partial class LaunchOptions : DbStoreOptions, IRecipient { + private readonly ITaskContext taskContext; + private readonly int primaryScreenWidth; private readonly int primaryScreenHeight; private readonly int primaryScreenFps; private string? gamePath; - private ImmutableList? gamePathEntries; + private ImmutableArray? gamePathEntries; + private bool? usingHoyolabAccount; - private bool? isEnabled; - private bool? isAdvancedLaunchOptionsEnabled; + private bool? areCommandLineArgumentsEnabled; private bool? isFullScreen; private bool? isBorderless; private bool? isExclusive; @@ -35,11 +40,18 @@ internal sealed partial class LaunchOptions : DbStoreOptions private bool? isScreenWidthEnabled; private int? screenHeight; private bool? isScreenHeightEnabled; - private bool? unlockFps; - private int? targetFps; + + private bool? isIslandEnabled; + private bool? hookingSetFieldOfView; + private bool? isSetFieldOfViewEnabled; private float? targetFov; + private bool? fixLowFovScene; private bool? disableFog; - private bool? loopAdjustFpsOnly; + private bool? isSetTargetFrameRateEnabled; + private int? targetFps; + private bool? hookingOpenTeam; + private bool? removeOpenTeamProgress; + private bool? hookingMickyWonderPartner2; private NameValue? monitor; private bool? isMonitorEnabled; private bool? usingCloudThirdPartyMobile; @@ -52,11 +64,13 @@ internal sealed partial class LaunchOptions : DbStoreOptions public LaunchOptions(IServiceProvider serviceProvider) : base(serviceProvider) { + taskContext = serviceProvider.GetRequiredService(); + RectInt32 primaryRect = DisplayArea.Primary.OuterBounds; primaryScreenWidth = primaryRect.Width; primaryScreenHeight = primaryRect.Height; - InitializeMonitors(Monitors); + Monitors = InitializeMonitors(); InitializeScreenFps(out primaryScreenFps); // Batch initialization, boost up performance @@ -65,7 +79,7 @@ public LaunchOptions(IServiceProvider serviceProvider) _ = key switch { SettingEntry.LaunchUsingHoyolabAccount => InitializeBooleanValue(ref usingHoyolabAccount, value), - SettingEntry.LaunchIsLaunchOptionsEnabled => InitializeBooleanValue(ref isEnabled, value), + SettingEntry.LaunchAreCommandLineArgumentsEnabled => InitializeBooleanValue(ref areCommandLineArgumentsEnabled, value), SettingEntry.LaunchIsFullScreen => InitializeBooleanValue(ref isFullScreen, value), SettingEntry.LaunchIsBorderless => InitializeBooleanValue(ref isBorderless, value), SettingEntry.LaunchIsExclusive => InitializeBooleanValue(ref isExclusive, value), @@ -73,21 +87,30 @@ public LaunchOptions(IServiceProvider serviceProvider) SettingEntry.LaunchIsScreenWidthEnabled => InitializeBooleanValue(ref isScreenWidthEnabled, value), SettingEntry.LaunchScreenHeight => InitializeInt32Value(ref screenHeight, value), SettingEntry.LaunchIsScreenHeightEnabled => InitializeBooleanValue(ref isScreenHeightEnabled, value), - SettingEntry.LaunchUnlockFps => InitializeBooleanValue(ref unlockFps, value), - SettingEntry.LaunchTargetFps => InitializeInt32Value(ref targetFps, value), - SettingEntry.LaunchTargetFov => InitializeFloatValue(ref targetFov, value), - SettingEntry.LaunchDisableFog => InitializeBooleanValue(ref disableFog, value), SettingEntry.LaunchIsMonitorEnabled => InitializeBooleanValue(ref isMonitorEnabled, value), SettingEntry.LaunchUsingCloudThirdPartyMobile => InitializeBooleanValue(ref usingCloudThirdPartyMobile, value), SettingEntry.LaunchIsWindowsHDREnabled => InitializeBooleanValue(ref isWindowsHDREnabled, value), SettingEntry.LaunchUsingStarwardPlayTimeStatistics => InitializeBooleanValue(ref usingStarwardPlayTimeStatistics, value), SettingEntry.LaunchUsingBetterGenshinImpactAutomation => InitializeBooleanValue(ref usingBetterGenshinImpactAutomation, value), SettingEntry.LaunchSetDiscordActivityWhenPlaying => InitializeBooleanValue(ref setDiscordActivityWhenPlaying, value), - SettingEntry.LaunchLoopAdjustFpsOnly => InitializeBooleanValue(ref loopAdjustFpsOnly, value), + SettingEntry.LaunchIsIslandEnabled => InitializeBooleanValue(ref isIslandEnabled, value), + SettingEntry.LaunchHookingSetFieldOfView => InitializeBooleanValue(ref hookingSetFieldOfView, value), + SettingEntry.LaunchIsSetFieldOfViewEnabled => InitializeBooleanValue(ref isSetFieldOfViewEnabled, value), + SettingEntry.LaunchTargetFov => InitializeFloatValue(ref targetFov, value), + SettingEntry.LaunchFixLowFovScene => InitializeBooleanValue(ref fixLowFovScene, value), + SettingEntry.LaunchDisableFog => InitializeBooleanValue(ref disableFog, value), + SettingEntry.LaunchIsSetTargetFrameRateEnabled => InitializeBooleanValue(ref isSetTargetFrameRateEnabled, value), + SettingEntry.LaunchTargetFps => InitializeInt32Value(ref targetFps, value), + SettingEntry.LaunchHookingOpenTeam => InitializeBooleanValue(ref hookingOpenTeam, value), + SettingEntry.LaunchRemoveOpenTeamProgress => InitializeBooleanValue(ref removeOpenTeamProgress, value), + SettingEntry.LaunchHookingMickyWonderPartner2 => InitializeBooleanValue(ref hookingMickyWonderPartner2, value), _ => default, }; }); + IslandFeatureStateMachine = new(this); + serviceProvider.GetRequiredService().Register(this); + static Void InitializeBooleanValue(ref bool? storage, string? value) { if (value is not null) @@ -118,8 +141,9 @@ static Void InitializeFloatValue(ref float? storage, string? value) return default; } - static void InitializeMonitors(List> monitors) + static ImmutableArray> InitializeMonitors() { + ImmutableArray>.Builder monitors = ImmutableArray.CreateBuilder>(); try { // This list can't use foreach @@ -136,6 +160,8 @@ static void InitializeMonitors(List> monitors) { monitors.Clear(); } + + return monitors.ToImmutable(); } static void InitializeScreenFps(out int fps) @@ -159,31 +185,37 @@ public string GamePath set => SetOption(ref gamePath, SettingEntry.GamePath, value); } - public ImmutableList GamePathEntries + public ImmutableArray GamePathEntries { // Because DbStoreOptions can't detect collection change, We use // ImmutableList to imply that the whole list needs to be replaced - get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize>(raw), []); + get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize>(raw), []).Value; set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, v => JsonSerializer.Serialize(v)); } - public bool IsAdvancedLaunchOptionsEnabled - { - get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled); - set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value); - } - #region Launch Prefixed Options + + #region CLI Options + public bool UsingHoyolabAccount { get => GetOption(ref usingHoyolabAccount, SettingEntry.LaunchUsingHoyolabAccount, false); set => SetOption(ref usingHoyolabAccount, SettingEntry.LaunchUsingHoyolabAccount, value); } - public bool IsEnabled + public bool AreCommandLineArgumentsEnabled { - get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true); - set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); + get => GetOption(ref areCommandLineArgumentsEnabled, SettingEntry.LaunchAreCommandLineArgumentsEnabled, true); + set + { + if (SetOption(ref areCommandLineArgumentsEnabled, SettingEntry.LaunchAreCommandLineArgumentsEnabled, value)) + { + if (!value) + { + UsingHoyolabAccount = false; + } + } + } } public bool IsFullScreen @@ -228,35 +260,7 @@ public bool IsScreenHeightEnabled set => SetOption(ref isScreenHeightEnabled, SettingEntry.LaunchIsScreenHeightEnabled, value); } - public bool UnlockFps - { - get => GetOption(ref unlockFps, SettingEntry.LaunchUnlockFps); - set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value); - } - - public int TargetFps - { - get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps); - set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value); - } - - public float TargetFov - { - get => GetOption(ref targetFov, SettingEntry.LaunchTargetFov, 45f); - set => SetOption(ref targetFov, SettingEntry.LaunchTargetFov, value); - } - - public bool DisableFog - { - get => GetOption(ref disableFog, SettingEntry.LaunchDisableFog, false); - set => SetOption(ref disableFog, SettingEntry.LaunchDisableFog, value); - } - - public bool LoopAdjustFpsOnly - { - get => GetOption(ref loopAdjustFpsOnly, SettingEntry.LaunchLoopAdjustFpsOnly, true); - set => SetOption(ref loopAdjustFpsOnly, SettingEntry.LaunchLoopAdjustFpsOnly, value); - } + public ImmutableArray> Monitors { get; } [NotNull] public NameValue? Monitor @@ -265,9 +269,9 @@ public NameValue? Monitor { return GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[RestrictIndex(Monitors, index)], Monitors[0]); - static int RestrictIndex(List> monitors, string index) + static int RestrictIndex(ImmutableArray> monitors, string index) { - return Math.Clamp(int.Parse(index, CultureInfo.InvariantCulture) - 1, 0, monitors.Count - 1); + return Math.Clamp(int.Parse(index, CultureInfo.InvariantCulture) - 1, 0, monitors.Length - 1); } } @@ -297,6 +301,9 @@ public bool IsWindowsHDREnabled get => GetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, false); set => SetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, value); } + #endregion + + #region InterProcess public bool UsingStarwardPlayTimeStatistics { @@ -317,9 +324,110 @@ public bool SetDiscordActivityWhenPlaying } #endregion - public List> Monitors { get; } = []; + #region Island Features + + public LaunchOptionsIslandFeatureStateMachine IslandFeatureStateMachine { get; } + + public bool IsIslandEnabled + { + get => GetOption(ref isIslandEnabled, SettingEntry.LaunchIsIslandEnabled, false); + set + { + if (SetOption(ref isIslandEnabled, SettingEntry.LaunchIsIslandEnabled, value)) + { + IslandFeatureStateMachine.Update(this); + } + } + } + + public bool HookingSetFieldOfView + { + get => GetOption(ref hookingSetFieldOfView, SettingEntry.LaunchHookingSetFieldOfView, true); + set + { + if (SetOption(ref hookingSetFieldOfView, SettingEntry.LaunchHookingSetFieldOfView, value)) + { + IslandFeatureStateMachine.Update(this); + } + } + } + + public bool IsSetFieldOfViewEnabled + { + get => GetOption(ref isSetFieldOfViewEnabled, SettingEntry.LaunchIsSetFieldOfViewEnabled, true); + set + { + if (SetOption(ref isSetFieldOfViewEnabled, SettingEntry.LaunchIsSetFieldOfViewEnabled, value)) + { + IslandFeatureStateMachine.Update(this); + } + } + } + + public float TargetFov + { + get => GetOption(ref targetFov, SettingEntry.LaunchTargetFov, 45f); + set => SetOption(ref targetFov, SettingEntry.LaunchTargetFov, value); + } + + public bool FixLowFovScene + { + get => GetOption(ref fixLowFovScene, SettingEntry.LaunchFixLowFovScene, true); + set => SetOption(ref fixLowFovScene, SettingEntry.LaunchFixLowFovScene, value); + } + + public bool DisableFog + { + get => GetOption(ref disableFog, SettingEntry.LaunchDisableFog, false); + set => SetOption(ref disableFog, SettingEntry.LaunchDisableFog, value); + } + + public bool IsSetTargetFrameRateEnabled + { + get => GetOption(ref isSetTargetFrameRateEnabled, SettingEntry.LaunchIsSetTargetFrameRateEnabled, true); + set + { + if (SetOption(ref isSetTargetFrameRateEnabled, SettingEntry.LaunchIsSetTargetFrameRateEnabled, value)) + { + IslandFeatureStateMachine.Update(this); + } + } + } + + public int TargetFps + { + get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps); + set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value); + } + + public bool HookingOpenTeam + { + get => GetOption(ref hookingOpenTeam, SettingEntry.LaunchHookingOpenTeam, true); + set + { + if (SetOption(ref hookingOpenTeam, SettingEntry.LaunchHookingOpenTeam, value)) + { + IslandFeatureStateMachine.Update(this); + } + } + } + + public bool RemoveOpenTeamProgress + { + get => GetOption(ref removeOpenTeamProgress, SettingEntry.LaunchRemoveOpenTeamProgress, false); + set => SetOption(ref removeOpenTeamProgress, SettingEntry.LaunchRemoveOpenTeamProgress, value); + } + + public bool HookingMickyWonderPartner2 + { + get => GetOption(ref hookingMickyWonderPartner2, SettingEntry.LaunchHookingMickyWonderPartner2, true); + set => SetOption(ref hookingMickyWonderPartner2, SettingEntry.LaunchHookingMickyWonderPartner2, value); + } + #endregion + + #endregion - public List AspectRatios { get; } = + public ImmutableArray AspectRatios { get; } = [ new(3840, 2160), new(2560, 1600), @@ -339,4 +447,17 @@ public AspectRatio? SelectedAspectRatio } } } + +#pragma warning disable CA1822 + public bool IsGameRunning { get => LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(); } +#pragma warning restore CA1822 + + public void Receive(LaunchExecutionProcessStatusChangedMessage message) + { + taskContext.BeginInvokeOnMainThread(() => + { + IslandFeatureStateMachine.Update(this); + OnPropertyChanged(nameof(IsGameRunning)); + }); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs index dff47349e2..ee1b5d81e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Service.Game.PathAbstraction; using System.Collections.Immutable; +using System.Runtime.InteropServices; namespace Snap.Hutao.Service.Game; @@ -18,50 +19,49 @@ public static bool TryGetGameFileSystem(this LaunchOptions options, [NotNullWhen return false; } - fileSystem = new GameFileSystem(gamePath); + fileSystem = new(gamePath); return true; } - public static ImmutableList GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry) + public static ImmutableArray GetGamePathEntries(this LaunchOptions options, out GamePathEntry? selected) { string gamePath = options.GamePath; if (string.IsNullOrEmpty(gamePath)) { - entry = default; + selected = default; return options.GamePathEntries; } if (options.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, options.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed) { - entry = existed; + selected = existed; return options.GamePathEntries; } - entry = GamePathEntry.Create(options.GamePath); - return [.. options.GamePathEntries, entry]; + selected = GamePathEntry.Create(options.GamePath); + return options.GamePathEntries = options.GamePathEntries.Add(selected); } - public static ImmutableList RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected) + public static ImmutableArray RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected) { - if (entry is not null) + if (entry is null) { - if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase)) - { - options.GamePath = string.Empty; - } + return options.GetGamePathEntries(out selected); + } - options.GamePathEntries = options.GamePathEntries.Remove(entry); + if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase)) + { + options.GamePath = string.Empty; } + options.GamePathEntries = options.GamePathEntries.Remove(entry); return options.GetGamePathEntries(out selected); } - public static ImmutableList UpdateGamePathAndRefreshEntries(this LaunchOptions options, string gamePath) + public static ImmutableArray UpdateGamePath(this LaunchOptions options, string gamePath) { options.GamePath = gamePath; - ImmutableList entries = options.GetGamePathEntries(out _); - options.GamePathEntries = entries; - return entries; + return options.GetGamePathEntries(out _); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsIslandFeatureStateMachine.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsIslandFeatureStateMachine.cs new file mode 100644 index 0000000000..670beb721d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsIslandFeatureStateMachine.cs @@ -0,0 +1,83 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Snap.Hutao.Service.Game; + +internal sealed partial class LaunchOptionsIslandFeatureStateMachine : ObservableObject +{ + private bool canInputTargetFov; + private bool canToggleSetFovHotSwitch; + private bool canToggleSetFovColdSwitch; + private bool canToggleFixLowFovHotSwitch; + private bool canToggleDisableFogHotSwitch; + private bool canToggleTeamHotSwitch; + private bool canToggleTeamColdSwitch; + private bool canToggleLetMeInColdSwitch; + private bool canInputTargetFps; + private bool canToggleSetFpsHotSwitch; + + public LaunchOptionsIslandFeatureStateMachine(LaunchOptions options) + { + Update(options); + } + + public bool CanInputTargetFov { get => canInputTargetFov; set => SetProperty(ref canInputTargetFov, value); } + + public bool CanToggleSetFovHotSwitch { get => canToggleSetFovHotSwitch; set => SetProperty(ref canToggleSetFovHotSwitch, value); } + + public bool CanToggleSetFovColdSwitch { get => canToggleSetFovColdSwitch; set => SetProperty(ref canToggleSetFovColdSwitch, value); } + + public bool CanToggleFixLowFovHotSwitch { get => canToggleFixLowFovHotSwitch; set => SetProperty(ref canToggleFixLowFovHotSwitch, value); } + + public bool CanToggleDisableFogHotSwitch { get => canToggleDisableFogHotSwitch; set => SetProperty(ref canToggleDisableFogHotSwitch, value); } + + public bool CanToggleTeamHotSwitch { get => canToggleTeamHotSwitch; set => SetProperty(ref canToggleTeamHotSwitch, value); } + + public bool CanToggleTeamColdSwitch { get => canToggleTeamColdSwitch; set => SetProperty(ref canToggleTeamColdSwitch, value); } + + public bool CanToggleLetMeInColdSwitch { get => canToggleLetMeInColdSwitch; set => SetProperty(ref canToggleLetMeInColdSwitch, value); } + + public bool CanInputTargetFps { get => canInputTargetFps; set => SetProperty(ref canInputTargetFps, value); } + + public bool CanToggleSetFpsHotSwitch { get => canToggleSetFpsHotSwitch; set => SetProperty(ref canToggleSetFpsHotSwitch, value); } + + public void Update(LaunchOptions options) + { + ( + CanInputTargetFov, + CanToggleSetFovHotSwitch, + CanToggleSetFovColdSwitch, + CanToggleFixLowFovHotSwitch, + CanToggleDisableFogHotSwitch, + CanToggleTeamHotSwitch, + CanToggleTeamColdSwitch, + CanToggleLetMeInColdSwitch, + CanInputTargetFps, + CanToggleSetFpsHotSwitch) = + (options.IsIslandEnabled, options.IsGameRunning, options.HookingSetFieldOfView, options.IsSetFieldOfViewEnabled, options.HookingOpenTeam, options.IsSetTargetFrameRateEnabled) switch + { + (false, _, _, _, _, _) => (false, false, false, false, false, false, false, false, false, false), + (true, false, false, _, false, true) => (false, false, true, false, false, false, true, true, true, true), + (true, false, false, _, true, true) => (false, false, true, false, false, true, true, true, true, true), + (true, false, false, _, false, false) => (false, false, true, false, false, false, true, true, false, true), + (true, false, false, _, true, false) => (false, false, true, false, false, true, true, true, false, true), + (true, false, true, false, false, _) => (false, true, true, false, false, false, true, true, false, false), + (true, false, true, false, true, _) => (false, true, true, false, false, true, true, true, false, false), + (true, false, true, true, false, false) => (true, true, true, true, true, false, true, true, false, true), + (true, false, true, true, false, true) => (true, true, true, true, true, false, true, true, true, true), + (true, false, true, true, true, false) => (true, true, true, true, true, true, true, true, false, true), + (true, false, true, true, true, true) => (true, true, true, true, true, true, true, true, true, true), + (true, true, false, _, false, false) => (false, false, false, false, false, false, false, false, false, true), + (true, true, false, _, false, true) => (false, false, false, false, false, false, false, false, true, true), + (true, true, false, _, true, false) => (false, false, false, false, false, true, false, false, false, true), + (true, true, false, _, true, true) => (false, false, false, false, false, true, false, false, true, true), + (true, true, true, false, false, _) => (false, true, false, false, false, false, false, false, false, false), + (true, true, true, false, true, _) => (false, true, false, false, false, true, false, false, false, false), + (true, true, true, true, false, _) => (true, true, false, false, false, true, false, false, false, true), + (true, true, true, true, true, false) => (true, true, false, true, true, true, false, false, false, true), + (true, true, true, true, true, true) => (true, true, false, true, true, true, false, false, true, true), + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs index 1cbb2c832b..016b675568 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs @@ -7,6 +7,11 @@ namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler { + public static bool IsGameRunning() + { + return IsGameRunning(out _); + } + public static bool IsGameRunning([NotNullWhen(true)] out Process? runningProcess) { int currentSessionId = Process.GetCurrentProcess().SessionId; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs index 489395ff4e..b726ff8252 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs @@ -2,20 +2,24 @@ // Licensed under the MIT license. using Microsoft.Win32.SafeHandles; +using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.Setting; using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Factory.Progress; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Package.Advanced; using Snap.Hutao.UI.Xaml.Control; using Snap.Hutao.UI.Xaml.View.Dialog; using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch; using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK; using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile; using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package; using Snap.Hutao.Web.Response; using System.IO; +using System.Net.Http; namespace Snap.Hutao.Service.Game.Launching.Handler; @@ -32,13 +36,14 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx { IServiceProvider serviceProvider = context.ServiceProvider; IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService(); - IProgressFactory progressFactory = serviceProvider.GetRequiredService(); LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - IProgress convertProgress = progressFactory.CreateForMainThread(state => dialog.State = state); - - using (await dialog.BlockAsync(contentDialogFactory).ConfigureAwait(false)) + using (await contentDialogFactory.BlockAsync(dialog).ConfigureAwait(false)) { + IProgress convertProgress = serviceProvider + .GetRequiredService() + .CreateForMainThread(state => dialog.State = state); + if (!await EnsureGameResourceAsync(context, gameFileSystem, convertProgress).ConfigureAwait(false)) { // context.Result is set in EnsureGameResourceAsync @@ -65,15 +70,15 @@ private static bool ShouldConvert(LaunchExecutionContext context, GameFileSystem } // Executable name not match - if (!context.Scheme.ExecutableMatches(gameFileSystem.GameFileName)) + if (!context.TargetScheme.ExecutableMatches(gameFileSystem.GameFileName)) { return true; } - if (!context.Scheme.IsOversea) + if (!context.TargetScheme.IsOversea) { // [It's Bilibili channel xor PCGameSDK.dll exists] means we need to convert - if (context.Scheme.Channel is ChannelType.Bili ^ File.Exists(gameFileSystem.PCGameSDKFilePath)) + if (context.TargetScheme.Channel is ChannelType.Bili ^ File.Exists(gameFileSystem.PCGameSDKFilePath)) { return true; } @@ -85,8 +90,6 @@ private static bool ShouldConvert(LaunchExecutionContext context, GameFileSystem private static async ValueTask EnsureGameResourceAsync(LaunchExecutionContext context, GameFileSystem gameFileSystem, IProgress progress) { string gameFolder = gameFileSystem.GameDirectory; - string gameFileName = gameFileSystem.GameFileName; - context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder); if (!CheckDirectoryPermissions(gameFolder)) @@ -100,16 +103,7 @@ private static async ValueTask EnsureGameResourceAsync(LaunchExecutionCont HoyoPlayClient hoyoPlayClient = context.ServiceProvider.GetRequiredService(); - // We perform these requests before package conversion to ensure resources index is intact. - Response packagesResponse = await hoyoPlayClient.GetPackagesAsync(context.Scheme).ConfigureAwait(false); - if (!ResponseValidator.TryValidateWithoutUINotification(packagesResponse, out GamePackagesWrapper? gamePackages)) - { - context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; - context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(packagesResponse); - return false; - } - - Response sdkResponse = await hoyoPlayClient.GetChannelSDKAsync(context.Scheme).ConfigureAwait(false); + Response sdkResponse = await hoyoPlayClient.GetChannelSDKAsync(context.TargetScheme).ConfigureAwait(false); if (!ResponseValidator.TryValidateWithoutUINotification(sdkResponse, out GameChannelSDKsWrapper? channelSDKs)) { context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; @@ -117,7 +111,7 @@ private static async ValueTask EnsureGameResourceAsync(LaunchExecutionCont return false; } - Response deprecatedFileResponse = await hoyoPlayClient.GetDeprecatedFileConfigurationsAsync(context.Scheme).ConfigureAwait(false); + Response deprecatedFileResponse = await hoyoPlayClient.GetDeprecatedFileConfigurationsAsync(context.TargetScheme).ConfigureAwait(false); if (!ResponseValidator.TryValidateWithoutUINotification(deprecatedFileResponse, out DeprecatedFileConfigurationsWrapper? deprecatedFileConfigs)) { context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; @@ -125,26 +119,81 @@ private static async ValueTask EnsureGameResourceAsync(LaunchExecutionCont return false; } - PackageConverter packageConverter = context.ServiceProvider.GetRequiredService(); - - if (!context.Scheme.ExecutableMatches(gameFileName)) + IHttpClientFactory httpClientFactory = context.ServiceProvider.GetRequiredService(); + using (HttpClient httpClient = httpClientFactory.CreateClient(GamePackageService.HttpClientName)) { - if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, gamePackages.GamePackages.Single(), gameFolder, progress).ConfigureAwait(false)) + PackageConverterType type = context.ServiceProvider.GetRequiredService().PackageConverterType; + + PackageConverterContext.CommonReferences common = new( + httpClient, + context.CurrentScheme, + context.TargetScheme, + gameFileSystem, + channelSDKs.GameChannelSDKs.SingleOrDefault(), + deprecatedFileConfigs.DeprecatedFileConfigurations.SingleOrDefault(), + progress); + + PackageConverterContext packageConverterContext; + switch (type) { - context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError; - context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail; - return false; + case PackageConverterType.ScatteredFiles: + Response packagesResponse = await hoyoPlayClient.GetPackagesAsync(context.TargetScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(packagesResponse, out GamePackagesWrapper? gamePackages)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(packagesResponse); + return false; + } + + packageConverterContext = new(common, gamePackages.GamePackages.Single()); + break; + case PackageConverterType.SophonChunks: + Response currentBranchesResponse = await hoyoPlayClient.GetBranchesAsync(context.CurrentScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(currentBranchesResponse, out GameBranchesWrapper? currentBranches)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(currentBranchesResponse); + return false; + } + + Response targetBranchesResponse = await hoyoPlayClient.GetBranchesAsync(context.TargetScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(targetBranchesResponse, out GameBranchesWrapper? targetBranches)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(targetBranchesResponse); + return false; + } + + packageConverterContext = new( + common, + currentBranches.GameBranches.Single(b => b.Game.Id == context.CurrentScheme.GameId).Main, + targetBranches.GameBranches.Single(b => b.Game.Id == context.TargetScheme.GameId).Main); + break; + default: + throw HutaoException.NotSupported(); } - // We need to change the gamePath if we switched. - string executableName = context.Scheme.IsOversea ? GameConstants.GenshinImpactFileName : GameConstants.YuanShenFileName; + IPackageConverter packageConverter = context.ServiceProvider.GetRequiredKeyedService(type); - await context.TaskContext.SwitchToMainThreadAsync(); - context.Options.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, executableName)); - } + if (!context.TargetScheme.ExecutableMatches(gameFileSystem.GameFileName)) + { + if (!await packageConverter.EnsureGameResourceAsync(packageConverterContext).ConfigureAwait(false)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError; + context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail; + return false; + } - await packageConverter.EnsureDeprecatedFilesAndSdkAsync(channelSDKs.GameChannelSDKs.SingleOrDefault(), deprecatedFileConfigs.DeprecatedFileConfigurations.SingleOrDefault(), gameFolder).ConfigureAwait(false); - return true; + // We need to change the gamePath if we switched. + string executableName = context.TargetScheme.IsOversea ? GameConstants.GenshinImpactFileName : GameConstants.YuanShenFileName; + + await context.TaskContext.SwitchToMainThreadAsync(); + context.Options.UpdateGamePath(Path.Combine(gameFolder, executableName)); + } + + await packageConverter.EnsureDeprecatedFilesAndSdkAsync(packageConverterContext).ConfigureAwait(false); + return true; + } } private static bool CheckDirectoryPermissions(string folder) @@ -180,4 +229,4 @@ private static bool CheckDirectoryPermissions(string folder) return false; } } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeHandler.cs index 5336e9f2f9..9a205392ca 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeHandler.cs @@ -7,14 +7,14 @@ internal sealed class LaunchExecutionEnsureSchemeHandler : ILaunchExecutionDeleg { public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) { - if (context.Scheme is null) + if (context.TargetScheme is null) { context.Result.Kind = LaunchExecutionResultKind.NoActiveScheme; context.Result.ErrorMessage = SH.ViewModelLaunchGameSchemeNotSelected; return; } - context.Logger.LogInformation("Scheme [{Scheme}] is selected", context.Scheme.DisplayName); + context.Logger.LogInformation("TargetScheme [{TargetScheme}] is selected", context.TargetScheme.DisplayName); await next().ConfigureAwait(false); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs index cbd10715af..0e5ac8969d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs @@ -1,20 +1,35 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; + namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionGameProcessExitHandler : ILaunchExecutionDelegateHandler { public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) { - if (!context.Process.HasExited) + try + { + if (!context.Process.HasExited) + { + context.Progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); + await context.Process.WaitForExitAsync().ConfigureAwait(false); + } + + context.Logger.LogInformation("Game process exited with code {ExitCode}", context.Process.ExitCode); + context.Progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + await next().ConfigureAwait(false); + } + finally { - context.Progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); - await context.Process.WaitForExitAsync().ConfigureAwait(false); + SpinWaitGameRunning(); + context.ServiceProvider.GetRequiredService().Send(); } + } - context.Logger.LogInformation("Game process exited with code {ExitCode}", context.Process.ExitCode); - context.Progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); - await next().ConfigureAwait(false); + private static unsafe void SpinWaitGameRunning() + { + SpinWaitPolyfill.SpinWhile(&LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs index 06a5be37fd..751313d042 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs @@ -26,7 +26,7 @@ private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionC LaunchOptions launchOptions = context.Options; string commandLine = string.Empty; - if (launchOptions.IsEnabled) + if (launchOptions.AreCommandLineArgumentsEnabled) { // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs index 30b6c736eb..94a8e6733e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; using Snap.Hutao.Win32.Foundation; namespace Snap.Hutao.Service.Game.Launching.Handler; @@ -12,6 +13,7 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx try { context.Process.Start(); + context.ServiceProvider.GetRequiredService().Send(); context.Logger.LogInformation("Process started"); } catch (Win32Exception ex) when (ex.HResult == HRESULT.E_FAIL) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs index 031c3b95a4..ce3b7e5cfe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs @@ -50,13 +50,13 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx { if (parameter.Key is ChannelOptions.ChannelName) { - context.ChannelOptionsChanged = parameter.Set(context.Scheme.Channel.ToString("D")) || context.ChannelOptionsChanged; + context.ChannelOptionsChanged = parameter.Set(context.TargetScheme.Channel.ToString("D")) || context.ChannelOptionsChanged; continue; } if (parameter.Key is ChannelOptions.SubChannelName) { - context.ChannelOptionsChanged = parameter.Set(context.Scheme.SubChannel.ToString("D")) || context.ChannelOptionsChanged; + context.ChannelOptionsChanged = parameter.Set(context.TargetScheme.SubChannel.ToString("D")) || context.ChannelOptionsChanged; continue; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs index ecad554c7c..a76fc08a70 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs @@ -18,7 +18,7 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx context.Logger.LogInformation("Set discord activity as playing"); await context.ServiceProvider .GetRequiredService() - .SetPlayingActivityAsync(context.Scheme.IsOversea) + .SetPlayingActivityAsync(context.TargetScheme.IsOversea) .ConfigureAwait(false); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs index 4b522013ef..22d8d90bf4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs @@ -41,7 +41,7 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx private static async ValueTask HandleMiYouSheAccountAsync(LaunchExecutionContext context) { - if (context.Scheme.GetSchemeType() is SchemeType.ChineseBilibili) + if (context.TargetScheme.GetSchemeType() is SchemeType.ChineseBilibili) { context.Logger.LogWarning("Bilibili server does not support auth ticket login"); @@ -55,7 +55,7 @@ private static async ValueTask HandleMiYouSheAccountAsync(LaunchExecutionC return true; } - if (userAndUid.IsOversea ^ context.Scheme.IsOversea) + if (userAndUid.IsOversea ^ context.TargetScheme.IsOversea) { context.Result.Kind = LaunchExecutionResultKind.GameAccountUserAndUidAndServerNotMatch; context.Result.ErrorMessage = SH.ViewModelLaunchGameAccountAndServerNotMatch; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs index ea2b120564..a59b89c406 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs @@ -12,7 +12,7 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx if (context.Options.IsWindowsHDREnabled) { context.Logger.LogInformation("Set Windows HDR"); - RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea); + RegistryInterop.SetWindowsHDR(context.TargetScheme.IsOversea); } await next().ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs index 6c6c5502c8..80d9cd9cf1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs @@ -20,7 +20,7 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx private static async ValueTask LaunchStarwardForPlayTimeStatisticsAsync(LaunchExecutionContext context) { - string gameBiz = context.Scheme.IsOversea ? "hk4e_global" : "hk4e_cn"; + string gameBiz = context.TargetScheme.IsOversea ? "hk4e_global" : "hk4e_cn"; Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri(); if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs index 0a8456ce15..e22fdfc552 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core; +using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Service.Game.Unlocker; namespace Snap.Hutao.Service.Game.Launching.Handler; @@ -10,7 +11,7 @@ internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegate { public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) { - if (HutaoRuntime.IsProcessElevated && context.Options is { IsAdvancedLaunchOptionsEnabled: true, UnlockFps: true }) + if (HutaoRuntime.IsProcessElevated && context.Options.IsIslandEnabled) { context.Logger.LogInformation("Unlocking FPS"); context.Progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); @@ -35,7 +36,7 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx } else { - context.Logger.LogError("Unlocking FPS failed"); + HutaoException.Throw("下载解锁帧率配置文件失败"); } } catch (Exception ex) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs index 5d6de5896f..eef5292a64 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs @@ -21,11 +21,12 @@ internal sealed partial class LaunchExecutionContext private GameFileSystem? gameFileSystem; [SuppressMessage("", "SH007")] - public LaunchExecutionContext(IServiceProvider serviceProvider, IViewModelSupportLaunchExecution viewModel, LaunchScheme? scheme, GameAccount? account, UserAndUid? userAndUid) + public LaunchExecutionContext(IServiceProvider serviceProvider, IViewModelSupportLaunchExecution viewModel, LaunchScheme? targetScheme, GameAccount? account, UserAndUid? userAndUid) : this(serviceProvider) { ViewModel = viewModel; - Scheme = scheme!; + CurrentScheme = viewModel.Shared.GetCurrentLaunchSchemeFromConfigFile()!; + TargetScheme = targetScheme!; Account = account; UserAndUid = userAndUid; } @@ -44,7 +45,9 @@ public LaunchExecutionContext(IServiceProvider serviceProvider, IViewModelSuppor public IViewModelSupportLaunchExecution ViewModel { get; private set; } = default!; - public LaunchScheme Scheme { get; private set; } = default!; + public LaunchScheme CurrentScheme { get; private set; } = default!; + + public LaunchScheme TargetScheme { get; private set; } = default!; public GameAccount? Account { get; private set; } @@ -79,7 +82,7 @@ public bool TryGetGameFileSystem([NotNullWhen(true)] out GameFileSystem? gameFil public void UpdateGamePathEntry() { - ImmutableList gamePathEntries = Options.GetGamePathEntries(out GamePathEntry? selectedEntry); + ImmutableArray gamePathEntries = Options.GetGamePathEntries(out GamePathEntry? selectedEntry); ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selectedEntry); // invalidate game file system diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionProcessStatusChangedMessage.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionProcessStatusChangedMessage.cs new file mode 100644 index 0000000000..394e9f9116 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionProcessStatusChangedMessage.cs @@ -0,0 +1,6 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionProcessStatusChangedMessage; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/GameAssetOperationSSD.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/GameAssetOperationSSD.cs index 15d3d6d4fb..ddbc23fd55 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/GameAssetOperationSSD.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/GameAssetOperationSSD.cs @@ -19,7 +19,7 @@ public override async ValueTask InstallAssetsAsync(GamePackageServiceContext con await Parallel.ForEachAsync(remoteBuild.Manifests, context.ParallelOptions, async (manifest, token) => { IEnumerable assets = manifest.ManifestProto.Assets.Select(asset => SophonAssetOperation.AddOrRepair(manifest.UrlPrefix, asset)); - await Parallel.ForEachAsync(assets, context.ParallelOptions, (asset, token) => EnsureAssetAsync(context, asset)).ConfigureAwait(false); + await Parallel.ForEachAsync(assets, context.ParallelOptions, (asset, token) => EnsureAssetAsync(context, asset)).ConfigureAwait(true); }).ConfigureAwait(false); } @@ -91,7 +91,7 @@ private static async ValueTask MergeChunkIntoAssetAsync(GamePackageServiceContex return; } - using (await context.ExclusiveProcessChunkAsync(chunk.ChunkName, token).ConfigureAwait(false)) + using (await context.ExclusiveProcessChunkAsync(chunk.ChunkName, token).ConfigureAwait(true)) { using (FileStream chunkFile = File.OpenRead(chunkPath)) { @@ -100,13 +100,13 @@ private static async ValueTask MergeChunkIntoAssetAsync(GamePackageServiceContex long offset = chunk.ChunkOnFileOffset; do { - int bytesRead = await decompressionStream.ReadAsync(buffer, token).ConfigureAwait(false); + int bytesRead = await decompressionStream.ReadAsync(buffer, token).ConfigureAwait(true); if (bytesRead <= 0) { break; } - await RandomAccess.WriteAsync(fileHandle, buffer[..bytesRead], offset, token).ConfigureAwait(false); + await RandomAccess.WriteAsync(fileHandle, buffer[..bytesRead], offset, token).ConfigureAwait(true); context.Progress.Report(new GamePackageOperationReport.Install(bytesRead, 0, chunk.ChunkName)); offset += bytesRead; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/SophonAssetOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/SophonAssetOperation.cs index b7bfe95651..e6d4e77a9e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/SophonAssetOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/Advanced/SophonAssetOperation.cs @@ -24,6 +24,7 @@ public static SophonAssetOperation AddOrRepair(string urlPrefix, AssetProperty n Kind = SophonAssetOperationKind.AddOrRepair, UrlPrefix = string.Intern(urlPrefix), NewAsset = newAsset, + DiffChunks = newAsset.AssetChunks.Select(chunk => new SophonChunk(urlPrefix, chunk)).ToList(), }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs new file mode 100644 index 0000000000..ab39e5eda4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Package; + +internal interface IPackageConverter +{ + ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterContext context); + + ValueTask EnsureGameResourceAsync(PackageConverterContext context); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs new file mode 100644 index 0000000000..cc7aadda36 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs @@ -0,0 +1,171 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Concurrent; +using Snap.Hutao.Core; +using Snap.Hutao.Service.Game.Scheme; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package; +using System.IO; +using System.Net.Http; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game.Package; + +internal readonly struct PackageConverterContext +{ + public readonly CommonReferences Common; + public readonly ParallelOptions ParallelOptions; + + public readonly ScatteredFilesOnlyReferences ScatterFilesOnly; + public readonly SophonChunksOnlyReferences SophonChunksOnly; + + public readonly string ServerCacheFolder; + + public readonly string ServerCacheChunksFolder; + public readonly ConcurrentDictionary DuplicatedChunkNames = []; + + public readonly string ServerCacheBackupFolder; // From + public readonly string ServerCacheTargetFolder; // To + + public readonly string FromDataFolderName; + public readonly string ToDataFolderName; + public readonly string FromDataFolder; + public readonly string ToDataFolder; + + public readonly string? ScatteredFilesUrl; + public readonly string? PkgVersionUrl; + + private readonly AsyncKeyedLock chunkLocks = new(); + + public PackageConverterContext(CommonReferences common, BranchWrapper currentBranch, BranchWrapper targetBranch) + : this(common) + { + Common = common; + SophonChunksOnly = new(currentBranch, targetBranch); + } + + public PackageConverterContext(CommonReferences common, GamePackage gamePackage) + : this(common) + { + Common = common; + ScatterFilesOnly = new(gamePackage); + + ScatteredFilesUrl = gamePackage.Main.Major.ResourceListUrl; + PkgVersionUrl = $"{ScatteredFilesUrl}/pkg_version"; + } + + private PackageConverterContext(CommonReferences common) + { + ParallelOptions = new() { MaxDegreeOfParallelism = Environment.ProcessorCount, }; + + ServerCacheFolder = HutaoRuntime.GetDataFolderServerCacheFolder(); + ServerCacheChunksFolder = Path.Combine(ServerCacheFolder, "Chunks"); + + string serverCacheOversea = Path.Combine(ServerCacheFolder, "Oversea"); + string serverCacheChinese = Path.Combine(ServerCacheFolder, "Chinese"); + (ServerCacheBackupFolder, ServerCacheTargetFolder) = common.TargetScheme.IsOversea + ? (serverCacheChinese, serverCacheOversea) + : (serverCacheOversea, serverCacheChinese); + + (FromDataFolderName, ToDataFolderName) = common.TargetScheme.IsOversea + ? (YuanShenData, GenshinImpactData) + : (GenshinImpactData, YuanShenData); + + FromDataFolder = Path.Combine(common.GameFileSystem.GameDirectory, FromDataFolderName); + ToDataFolder = Path.Combine(common.GameFileSystem.GameDirectory, ToDataFolderName); + } + + public HttpClient HttpClient { get => Common.HttpClient; } + + public LaunchScheme CurrentScheme { get => Common.CurrentScheme; } + + public LaunchScheme TargetScheme { get => Common.TargetScheme; } + + public GameFileSystem GameFileSystem { get => Common.GameFileSystem; } + + public GameChannelSDK? GameChannelSDK { get => Common.GameChannelSDK; } + + public DeprecatedFilesWrapper? DeprecatedFiles { get => Common.DeprecatedFiles; } + + public IProgress Progress { get => Common.Progress; } + + public readonly string GetScatteredFilesUrl(string file) + { + return $"{ScatteredFilesUrl}/{file}"; + } + + public readonly string GetServerCacheBackupFilePath(string filePath) + { + return Path.Combine(ServerCacheBackupFolder, filePath); + } + + public readonly string GetServerCacheTargetFilePath(string filePath) + { + return Path.Combine(ServerCacheTargetFolder, filePath); + } + + public readonly string GetGameFolderFilePath(string filePath) + { + return Path.Combine(Common.GameFileSystem.GameDirectory, filePath); + } + + [SuppressMessage("", "SH003")] + public readonly Task.Releaser> ExclusiveProcessChunkAsync(string chunkName, CancellationToken token = default) + { + return chunkLocks.LockAsync(chunkName); + } + + internal readonly struct CommonReferences + { + public readonly HttpClient HttpClient; + public readonly LaunchScheme CurrentScheme; + public readonly LaunchScheme TargetScheme; + public readonly GameFileSystem GameFileSystem; + public readonly GameChannelSDK? GameChannelSDK; + public readonly DeprecatedFilesWrapper? DeprecatedFiles; + public readonly IProgress Progress; + + public CommonReferences( + HttpClient httpClient, + LaunchScheme currentScheme, + LaunchScheme targetScheme, + GameFileSystem gameFileSystem, + GameChannelSDK? gameChannelSDK, + DeprecatedFilesWrapper? deprecatedFiles, + IProgress progress) + { + HttpClient = httpClient; + CurrentScheme = currentScheme; + TargetScheme = targetScheme; + GameFileSystem = gameFileSystem; + GameChannelSDK = gameChannelSDK; + DeprecatedFiles = deprecatedFiles; + Progress = progress; + } + } + + internal readonly struct ScatteredFilesOnlyReferences + { + public readonly GamePackage? TargetPackage; + + public ScatteredFilesOnlyReferences(GamePackage? targetPackage) + { + TargetPackage = targetPackage; + } + } + + internal readonly struct SophonChunksOnlyReferences + { + public readonly BranchWrapper? CurrentBranch; + public readonly BranchWrapper? TargetBranch; + + public SophonChunksOnlyReferences(BranchWrapper currentBranch, BranchWrapper targetBranch) + { + CurrentBranch = currentBranch; + TargetBranch = targetBranch; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterFileSystemContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterFileSystemContext.cs deleted file mode 100644 index da78796481..0000000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterFileSystemContext.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.IO; -using static Snap.Hutao.Service.Game.GameConstants; - -namespace Snap.Hutao.Service.Game.Package; - -internal readonly struct PackageConverterFileSystemContext -{ - public readonly string GameFolder; - public readonly string ServerCacheFolder; - - public readonly string ServerCacheBackupFolder; // From - public readonly string ServerCacheTargetFolder; // To - - public readonly string FromDataFolderName; - public readonly string ToDataFolderName; - public readonly string FromDataFolder; - public readonly string ToDataFolder; - - public readonly string ScatteredFilesUrl; - public readonly string PkgVersionUrl; - - public PackageConverterFileSystemContext(bool isTargetOversea, string serverCacheFolder, string gameFolder, string scatteredFilesUrl) - { - GameFolder = gameFolder; - ServerCacheFolder = serverCacheFolder; - - string serverCacheOversea = Path.Combine(ServerCacheFolder, "Oversea"); - string serverCacheChinese = Path.Combine(ServerCacheFolder, "Chinese"); - (ServerCacheBackupFolder, ServerCacheTargetFolder) = isTargetOversea - ? (serverCacheChinese, serverCacheOversea) - : (serverCacheOversea, serverCacheChinese); - - (FromDataFolderName, ToDataFolderName) = isTargetOversea - ? (YuanShenData, GenshinImpactData) - : (GenshinImpactData, YuanShenData); - - FromDataFolder = Path.Combine(GameFolder, FromDataFolderName); - ToDataFolder = Path.Combine(GameFolder, ToDataFolderName); - - ScatteredFilesUrl = scatteredFilesUrl; - PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version"; - } - - public readonly string GetScatteredFilesUrl(string file) - { - return $"{ScatteredFilesUrl}/{file}"; - } - - public readonly string GetServerCacheBackupFilePath(string filePath) - { - return Path.Combine(ServerCacheBackupFolder, filePath); - } - - public readonly string GetServerCacheTargetFilePath(string filePath) - { - return Path.Combine(ServerCacheTargetFolder, filePath); - } - - public readonly string GetGameFolderFilePath(string filePath) - { - return Path.Combine(GameFolder, filePath); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterType.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterType.cs new file mode 100644 index 0000000000..5424cdc574 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterType.cs @@ -0,0 +1,10 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Package; + +internal enum PackageConverterType +{ + ScatteredFiles, + SophonChunks, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationForSophonChunks.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationForSophonChunks.cs new file mode 100644 index 0000000000..54d41e0e9c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationForSophonChunks.cs @@ -0,0 +1,52 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Package.Advanced; +using Snap.Hutao.Web.Hoyolab.Takumi.Downloader.Proto; + +namespace Snap.Hutao.Service.Game.Package; + +internal sealed class PackageItemOperationForSophonChunks +{ + public PackageItemOperationKind Kind { get; init; } + + public string UrlPrefix { get; init; } = default!; + + public AssetProperty OldAsset { get; init; } = default!; + + public AssetProperty NewAsset { get; init; } = default!; + + public List DiffChunks { get; init; } = []; + + public static PackageItemOperationForSophonChunks Add(string urlPrefix, AssetProperty newAsset) + { + return new() + { + Kind = PackageItemOperationKind.Add, + UrlPrefix = string.Intern(urlPrefix), + NewAsset = newAsset, + DiffChunks = newAsset.AssetChunks.Select(chunk => new SophonChunk(urlPrefix, chunk)).ToList(), + }; + } + + public static PackageItemOperationForSophonChunks ModifyOrReplace(string urlPrefix, AssetProperty oldAsset, AssetProperty newAsset, List diffChunks) + { + return new() + { + Kind = PackageItemOperationKind.Replace, + UrlPrefix = string.Intern(urlPrefix), + OldAsset = oldAsset, + NewAsset = newAsset, + DiffChunks = diffChunks, + }; + } + + public static PackageItemOperationForSophonChunks Backup(AssetProperty oldAsset) + { + return new() + { + Kind = PackageItemOperationKind.Backup, + OldAsset = oldAsset, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs similarity index 72% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs index 74b63242e3..e7ad83d3d0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs @@ -1,16 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core; -using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Http.Sharding; -using Snap.Hutao.Service.Game.Scheme; -using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK; using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile; -using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package; using System.Globalization; using System.IO; using System.IO.Compression; @@ -21,18 +16,16 @@ namespace Snap.Hutao.Service.Game.Package; -[ConstructorGenerated(ResolveHttpClient = true)] -[HttpClient(HttpClientConfiguration.Default)] -[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)] -internal sealed partial class PackageConverter +[ConstructorGenerated] +[Injection(InjectAs.Transient, typeof(IPackageConverter), Key = PackageConverterType.ScatteredFiles)] +internal sealed partial class ScatteredFilesPackageConverter : IPackageConverter { private const string PackageVersion = "pkg_version"; - private readonly ILogger logger; + private readonly ILogger logger; private readonly JsonSerializerOptions options; - private readonly HttpClient httpClient; - public async ValueTask EnsureGameResourceAsync(LaunchScheme targetScheme, GamePackage gamePackage, string gameFolder, IProgress progress) + public async ValueTask EnsureGameResourceAsync(PackageConverterContext context) { // 以 国服 -> 国际服 为例 // 1. 下载国际服的 pkg_version 文件,转换为索引字典 @@ -52,53 +45,48 @@ public async ValueTask EnsureGameResourceAsync(LaunchScheme targetScheme, // 4. 全部资源下载完成后,根据操作信息项,进行文件替换 // 处理顺序:备份/替换/新增 // 替换操作等于 先备份国服文件,随后新增国际服文件 - - // 准备下载链接 - string scatteredFilesUrl = gamePackage.Main.Major.ResourceListUrl; - string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}"; - - PackageConverterFileSystemContext context = new(targetScheme.IsOversea, HutaoRuntime.GetDataFolderServerCacheFolder(), gameFolder, scatteredFilesUrl); + ArgumentNullException.ThrowIfNull(context.ScatterFilesOnly.TargetPackage); // Step 1 - progress.Report(new(SH.ServiceGamePackageRequestPackageVerion)); - RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false); - RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false); + context.Progress.Report(new(SH.ServiceGamePackageRequestPackageVerion)); + RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(context).ConfigureAwait(false); + RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(context.GameFileSystem.GameDirectory).ConfigureAwait(false); // Step 2 List diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList(); diffOperations.SortBy(i => i.Kind); // Step 3 - await PrepareCacheFilesAsync(diffOperations, context, progress).ConfigureAwait(false); + await PrepareCacheFilesAsync(context, diffOperations).ConfigureAwait(false); // Step 4 - return await ReplaceGameResourceAsync(diffOperations, context, progress).ConfigureAwait(false); + return await ReplaceGameResourceAsync(context, diffOperations).ConfigureAwait(false); } - public async ValueTask EnsureDeprecatedFilesAndSdkAsync(GameChannelSDK? channelSDK, DeprecatedFilesWrapper? deprecatedFiles, string gameFolder) + public async ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterContext context) { // Just try to delete these files, always download from server when needed - FileOperation.Delete(Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll")); - FileOperation.Delete(Path.Combine(gameFolder, GenshinImpactData, "Plugins\\PCGameSDK.dll")); - FileOperation.Delete(Path.Combine(gameFolder, YuanShenData, "Plugins\\EOSSDK-Win64-Shipping.dll")); - FileOperation.Delete(Path.Combine(gameFolder, GenshinImpactData, "Plugins\\EOSSDK-Win64-Shipping.dll")); - FileOperation.Delete(Path.Combine(gameFolder, YuanShenData, "Plugins\\PluginEOSSDK.dll")); - FileOperation.Delete(Path.Combine(gameFolder, GenshinImpactData, "Plugins\\PluginEOSSDK.dll")); - FileOperation.Delete(Path.Combine(gameFolder, "sdk_pkg_version")); - - if (channelSDK is not null) + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, YuanShenData, "Plugins\\PCGameSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, GenshinImpactData, "Plugins\\PCGameSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, YuanShenData, "Plugins\\EOSSDK-Win64-Shipping.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, GenshinImpactData, "Plugins\\EOSSDK-Win64-Shipping.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, YuanShenData, "Plugins\\PluginEOSSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, GenshinImpactData, "Plugins\\PluginEOSSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, "sdk_pkg_version")); + + if (context.GameChannelSDK is not null) { - using (Stream sdkWebStream = await httpClient.GetStreamAsync(channelSDK.ChannelSdkPackage.Url).ConfigureAwait(false)) + using (Stream sdkWebStream = await context.HttpClient.GetStreamAsync(context.GameChannelSDK.ChannelSdkPackage.Url).ConfigureAwait(false)) { - ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true); + ZipFile.ExtractToDirectory(sdkWebStream, context.GameFileSystem.GameDirectory, true); } } - if (deprecatedFiles is not null) + if (context.DeprecatedFiles is not null) { - foreach (DeprecatedFile file in deprecatedFiles.DeprecatedFiles) + foreach (DeprecatedFile file in context.DeprecatedFiles.DeprecatedFiles) { - string filePath = Path.Combine(gameFolder, file.Name); + string filePath = Path.Combine(context.GameFileSystem.GameDirectory, file.Name); FileOperation.Move(filePath, $"{filePath}.backup", true); } } @@ -132,62 +120,7 @@ private static IEnumerable GetItemOperationInfos(Relat } } - [GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")] - private static partial Regex DataFolderRegex(); - - private async ValueTask GetVersionItemsAsync(Stream stream) - { - RelativePathVersionItemDictionary results = []; - using (StreamReader reader = new(stream)) - { - while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row) - { - VersionItem? item = JsonSerializer.Deserialize(row, options); - ArgumentNullException.ThrowIfNull(item); - item.RelativePath = DataFolderRegex().Replace(item.RelativePath, "{0}"); - results.Add(item.RelativePath, item); - } - } - - return results; - } - - private async ValueTask GetRemoteItemsAsync(string pkgVersionUrl) - { - try - { - // Server might close the connection shortly, - // we have to cache the content immediately. - using (HttpResponseMessage responseMessage = await httpClient.GetAsync(pkgVersionUrl, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false)) - { - using (Stream remoteSteam = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false)) - { - return await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false); - } - } - } - catch (IOException ex) - { - throw HutaoException.Throw(SH.ServiceGamePackageRequestPackageVerionFailed, ex); - } - } - - private async ValueTask GetLocalItemsAsync(string gameFolder) - { - try - { - using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion))) - { - return await GetVersionItemsAsync(localSteam).ConfigureAwait(false); - } - } - catch (JsonException ex) - { - throw HutaoException.Throw(SH.ServiceGamePackageReadLocalPackageVerionFailed, ex); - } - } - - private async ValueTask PrepareCacheFilesAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) + private static async ValueTask PrepareCacheFilesAsync(PackageConverterContext context, List operations) { foreach (PackageItemOperationInfo info in operations) { @@ -197,13 +130,13 @@ private async ValueTask PrepareCacheFilesAsync(List op continue; case PackageItemOperationKind.Replace: case PackageItemOperationKind.Add: - await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false); + await SkipOrDownloadAsync(context, info).ConfigureAwait(false); break; } } } - private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress progress) + private static async ValueTask SkipOrDownloadAsync(PackageConverterContext context, PackageItemOperationInfo info) { // 还原正确的远程地址 string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName); @@ -231,7 +164,7 @@ private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, Packa string remoteUrl = context.GetScatteredFilesUrl(remoteName); HttpShardCopyWorkerOptions options = new() { - HttpClient = httpClient, + HttpClient = context.HttpClient, SourceUrl = remoteUrl, DestinationFilePath = cacheFile, StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes), @@ -241,7 +174,7 @@ private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, Packa { try { - await worker.CopyAsync(progress).ConfigureAwait(false); + await worker.CopyAsync(context.Progress).ConfigureAwait(false); } catch (Exception ex) { @@ -257,7 +190,85 @@ private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, Packa } } - private async ValueTask ReplaceGameResourceAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) + private static async ValueTask ReplacePackageVersionFilesAsync(PackageConverterContext context) + { + foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFileSystem.GameDirectory, "*pkg_version")) + { + string versionFileName = Path.GetFileName(versionFilePath); + + if (string.Equals(versionFileName, "sdk_pkg_version", StringComparison.OrdinalIgnoreCase)) + { + // Skipping the sdk_pkg_version file, + // it can't be claimed from remote. + continue; + } + + using (FileStream versionFileStream = File.Create(versionFilePath)) + { + using (Stream webStream = await context.HttpClient.GetStreamAsync(context.GetScatteredFilesUrl(versionFileName)).ConfigureAwait(false)) + { + await webStream.CopyToAsync(versionFileStream).ConfigureAwait(false); + } + } + } + } + + [GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")] + private static partial Regex DataFolderRegex(); + + private async ValueTask GetVersionItemsAsync(Stream stream) + { + RelativePathVersionItemDictionary results = []; + using (StreamReader reader = new(stream)) + { + while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row) + { + VersionItem? item = JsonSerializer.Deserialize(row, options); + ArgumentNullException.ThrowIfNull(item); + item.RelativePath = DataFolderRegex().Replace(item.RelativePath, "{0}"); + results.Add(item.RelativePath, item); + } + } + + return results; + } + + private async ValueTask GetRemoteItemsAsync(PackageConverterContext context) + { + try + { + // Server might close the connection shortly, + // we have to cache the content immediately. + using (HttpResponseMessage responseMessage = await context.HttpClient.GetAsync(context.PkgVersionUrl, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false)) + { + using (Stream remoteSteam = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + return await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false); + } + } + } + catch (IOException ex) + { + throw HutaoException.Throw(SH.ServiceGamePackageRequestPackageVerionFailed, ex); + } + } + + private async ValueTask GetLocalItemsAsync(string gameFolder) + { + try + { + using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion))) + { + return await GetVersionItemsAsync(localSteam).ConfigureAwait(false); + } + } + catch (JsonException ex) + { + throw HutaoException.Throw(SH.ServiceGamePackageReadLocalPackageVerionFailed, ex); + } + } + + private async ValueTask ReplaceGameResourceAsync(PackageConverterContext context, List operations) { // 执行下载与移动操作 foreach (PackageItemOperationInfo info in operations) @@ -274,7 +285,7 @@ private async ValueTask ReplaceGameResourceAsync(List ReplaceGameResourceAsync(List ReplaceGameResourceAsync(List ReplaceGameResourceAsync(List logger; + private readonly IServiceProvider serviceProvider; + + public async ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterContext context) + { + // Just try to delete these files, always download from server when needed + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, YuanShenData, "Plugins\\PCGameSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, GenshinImpactData, "Plugins\\PCGameSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, YuanShenData, "Plugins\\EOSSDK-Win64-Shipping.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, GenshinImpactData, "Plugins\\EOSSDK-Win64-Shipping.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, YuanShenData, "Plugins\\PluginEOSSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, GenshinImpactData, "Plugins\\PluginEOSSDK.dll")); + FileOperation.Delete(Path.Combine(context.GameFileSystem.GameDirectory, "sdk_pkg_version")); + + if (context.GameChannelSDK is not null) + { + using (Stream sdkWebStream = await context.HttpClient.GetStreamAsync(context.GameChannelSDK.ChannelSdkPackage.Url).ConfigureAwait(false)) + { + ZipFile.ExtractToDirectory(sdkWebStream, context.GameFileSystem.GameDirectory, true); + } + } + + if (context.DeprecatedFiles is not null) + { + foreach (DeprecatedFile file in context.DeprecatedFiles.DeprecatedFiles) + { + string filePath = Path.Combine(context.GameFileSystem.GameDirectory, file.Name); + FileOperation.Move(filePath, $"{filePath}.backup", true); + } + } + } + + public async ValueTask EnsureGameResourceAsync(PackageConverterContext context) + { + // 基本步骤与 ScatteredPackageConverter 相同 + // 以 国服 -> 国际服 为例 + // 1. 获取两服的清单文件 + // + // 2. 对比两者差异,(类似更新处理) + // 国际服有 & 国服没有的 为新增 + // 国际服有 & 国服也有的 为替换或修补 + // 剩余国际服没有 & 国服有的 为备份 + // 生成对应的操作信息项,对比文件的尺寸与MD5 + // + // 3. 根据操作信息项,提取其中需要下载的项进行缓存对比或下载 + // 若缓存中文件的尺寸与MD5与操作信息项中的一致则直接跳过 + // 每个文件下载后需要验证文件文件的尺寸与MD5 + // 若出现下载失败的情况,终止转换进程,此时国服文件尚未替换 + // + // 4. 全部资源下载完成后,根据操作信息项,进行文件替换 + // 处理顺序:备份/替换/新增 + // 替换操作等于 先备份国服文件,随后新增国际服文件 + // 可能会存在大量相似代码,逻辑完成后再进行重构 + ArgumentNullException.ThrowIfNull(context.SophonChunksOnly.CurrentBranch); + ArgumentNullException.ThrowIfNull(context.SophonChunksOnly.TargetBranch); + + // Step 1 + context.Progress.Report(new("Decoding manifests...")); + SophonDecodedBuild? currentBuild = await DecodeManifestsAsync(context, context.SophonChunksOnly.CurrentBranch, context.CurrentScheme).ConfigureAwait(false); + SophonDecodedBuild? targetBuild = await DecodeManifestsAsync(context, context.SophonChunksOnly.TargetBranch, context.TargetScheme).ConfigureAwait(false); + ArgumentNullException.ThrowIfNull(currentBuild); + ArgumentNullException.ThrowIfNull(targetBuild); + + // Step 2 + List diffOperations = GetDiffOperations(currentBuild, targetBuild).ToList(); + diffOperations.SortBy(o => o.Kind); + InitializeDuplicatedChunkNames(context, diffOperations.SelectMany(a => a.DiffChunks.Select(c => c.AssetChunk))); + + // Step 3 + await PrepareCacheFilesAsync(context, diffOperations).ConfigureAwait(false); + + // Step 4 + return ReplaceGameResource(context, diffOperations); + } + + private static IEnumerable GetDiffOperations(SophonDecodedBuild currentDecodedBuild, SophonDecodedBuild targetDecodedBuild) + { + foreach ((SophonDecodedManifest currentManifest, SophonDecodedManifest targetManifest) in currentDecodedBuild.Manifests.Zip(targetDecodedBuild.Manifests)) + { + foreach (AssetProperty targetAsset in targetManifest.ManifestProto.Assets) + { + if (currentManifest.ManifestProto.Assets.FirstOrDefault(currentAsset => IsSameAsset(currentAsset, targetAsset)) is not { } currentAsset) + { + yield return PackageItemOperationForSophonChunks.Add(targetManifest.UrlPrefix, targetAsset); + continue; + } + + if (currentAsset.AssetHashMd5.Equals(targetAsset.AssetHashMd5, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + List diffChunks = []; + foreach (AssetChunk chunk in targetAsset.AssetChunks) + { + if (currentAsset.AssetChunks.FirstOrDefault(c => c.ChunkDecompressedHashMd5.Equals(chunk.ChunkDecompressedHashMd5, StringComparison.OrdinalIgnoreCase)) is null) + { + diffChunks.Add(new(targetManifest.UrlPrefix, chunk)); + } + } + + yield return PackageItemOperationForSophonChunks.ModifyOrReplace(targetManifest.UrlPrefix, currentAsset, targetAsset, diffChunks); + } + + foreach (AssetProperty currentAsset in currentManifest.ManifestProto.Assets) + { + if (targetManifest.ManifestProto.Assets.FirstOrDefault(a => IsSameAsset(a, currentAsset)) is null) + { + yield return PackageItemOperationForSophonChunks.Backup(currentAsset); + } + } + } + + static bool IsSameAsset(AssetProperty currentAsset, AssetProperty targetAsset) + { + // Ignore YuanShen_Data/ or GenshinImpact_Data/ + string currentAssetName = currentAsset.AssetName[(currentAsset.AssetName.IndexOf('/', StringComparison.OrdinalIgnoreCase) + 1)..]; + string targetAssetName = targetAsset.AssetName[(targetAsset.AssetName.IndexOf('/', StringComparison.OrdinalIgnoreCase) + 1)..]; + return currentAssetName.Equals(targetAssetName, StringComparison.OrdinalIgnoreCase); + } + } + + private static void InitializeDuplicatedChunkNames(PackageConverterContext context, IEnumerable chunks) + { + Debug.Assert(context.DuplicatedChunkNames.Count is 0); + IEnumerable names = chunks + .GroupBy(chunk => chunk.ChunkName) + .Where(group => group.Skip(1).Any()) + .Select(group => group.Key) + .Distinct(); + + foreach (string name in names) + { + context.DuplicatedChunkNames.TryAdd(name, default); + } + } + + private static async ValueTask DownloadChunksAsync(PackageConverterContext context, IEnumerable sophonChunks) + { + await Parallel.ForEachAsync(sophonChunks, context.ParallelOptions, (chunk, token) => DownloadChunkAsync(context, chunk)).ConfigureAwait(false); + } + + private static async ValueTask DownloadChunkAsync(PackageConverterContext context, SophonChunk sophonChunk) + { + Directory.CreateDirectory(context.ServerCacheChunksFolder); + string chunkPath = Path.Combine(context.ServerCacheChunksFolder, sophonChunk.AssetChunk.ChunkName); + + using (await context.ExclusiveProcessChunkAsync(sophonChunk.AssetChunk.ChunkName).ConfigureAwait(false)) + { + if (File.Exists(chunkPath)) + { + string chunkXxh64 = await XXH64.HashFileAsync(chunkPath).ConfigureAwait(false); + if (chunkXxh64.Equals(sophonChunk.AssetChunk.ChunkName.Split("_")[0], StringComparison.OrdinalIgnoreCase)) + { + return; + } + + File.Delete(chunkPath); + } + + using (FileStream fileStream = File.Create(chunkPath)) + { + fileStream.Position = 0; + + using (Stream webStream = await context.HttpClient.GetStreamAsync(sophonChunk.ChunkDownloadUrl).ConfigureAwait(false)) + { + using (StreamCopyWorker worker = new(webStream, fileStream, (_, totalBytesRead) => new PackageConvertStatus(sophonChunk.AssetChunk.ChunkName, totalBytesRead, sophonChunk.AssetChunk.ChunkSize))) + { + await worker.CopyAsync(context.Progress).ConfigureAwait(false); + + fileStream.Position = 0; + string chunkXxh64 = await XXH64.HashAsync(fileStream).ConfigureAwait(false); + if (chunkXxh64.Equals(sophonChunk.AssetChunk.ChunkName.Split("_")[0], StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + } + + private static async ValueTask MergeNewAssetAsync(PackageConverterContext context, AssetProperty assetProperty) + { + using (SafeFileHandle fileHandle = File.OpenHandle(context.GetServerCacheTargetFilePath(assetProperty.AssetName), FileMode.Create, FileAccess.Write, FileShare.None, preallocationSize: assetProperty.AssetSize)) + { + await Parallel.ForEachAsync(assetProperty.AssetChunks, context.ParallelOptions, (chunk, token) => MergeChunkIntoAssetAsync(context, fileHandle, chunk)).ConfigureAwait(false); + } + } + + private static async ValueTask MergeChunkIntoAssetAsync(PackageConverterContext context, SafeFileHandle fileHandle, AssetChunk chunk) + { + using (IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(81920)) + { + Memory buffer = memoryOwner.Memory; + + string chunkPath = Path.Combine(context.ServerCacheChunksFolder, chunk.ChunkName); + if (!File.Exists(chunkPath)) + { + return; + } + + using (await context.ExclusiveProcessChunkAsync(chunk.ChunkName).ConfigureAwait(true)) + { + using (FileStream chunkFile = File.OpenRead(chunkPath)) + { + using (ZstandardDecompressionStream decompressionStream = new(chunkFile)) + { + long offset = chunk.ChunkOnFileOffset; + do + { + int bytesRead = await decompressionStream.ReadAsync(buffer).ConfigureAwait(true); + if (bytesRead <= 0) + { + break; + } + + await RandomAccess.WriteAsync(fileHandle, buffer[..bytesRead], offset).ConfigureAwait(true); + offset += bytesRead; + } + while (true); + } + } + + if (!context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName)) + { + FileOperation.Delete(chunkPath); + } + } + } + } + + private async ValueTask DecodeManifestsAsync(PackageConverterContext context, BranchWrapper branch, LaunchScheme scheme) + { + SophonBuild? build; + using (IServiceScope scope = serviceProvider.CreateScope()) + { + ISophonClient client = scope.ServiceProvider + .GetRequiredService>() + .Create(scheme.IsOversea); + + Response response = await client.GetBuildAsync(branch).ConfigureAwait(false); + if (!ResponseValidator.TryValidate(response, serviceProvider, out build)) + { + return default!; + } + } + + SophonManifest sophonManifest = build.Manifests.Single(m => m.MatchingField == "game"); + string manifestDownloadUrl = $"{sophonManifest.ManifestDownload.UrlPrefix}/{sophonManifest.Manifest.Id}"; + using (Stream rawManifestStream = await context.HttpClient.GetStreamAsync(manifestDownloadUrl).ConfigureAwait(false)) + { + using (ZstandardDecompressionStream decompressor = new(rawManifestStream)) + { + using (MemoryStream inMemoryManifestStream = await memoryStreamFactory.GetStreamAsync(decompressor).ConfigureAwait(false)) + { + string manifestMd5 = await MD5.HashAsync(inMemoryManifestStream).ConfigureAwait(false); + if (!manifestMd5.Equals(sophonManifest.Manifest.Checksum, StringComparison.OrdinalIgnoreCase)) + { + return default!; + } + + inMemoryManifestStream.Position = 0; + SophonDecodedManifest decodedManifest = new(sophonManifest.ChunkDownload.UrlPrefix, SophonManifestProto.Parser.ParseFrom(inMemoryManifestStream)); + return new(sophonManifest.Stats.UncompressedSize, [decodedManifest]); + } + } + } + } + + private async ValueTask PrepareCacheFilesAsync(PackageConverterContext context, List operations) + { + foreach (PackageItemOperationForSophonChunks operation in operations) + { + ValueTask task = operation.Kind switch + { + PackageItemOperationKind.Replace or PackageItemOperationKind.Add => SkipOrProcessAsync(context, operation), + _ => ValueTask.CompletedTask, + }; + + await task.ConfigureAwait(false); + } + + Directory.Delete(context.ServerCacheChunksFolder, true); + } + + private async ValueTask SkipOrProcessAsync(PackageConverterContext context, PackageItemOperationForSophonChunks operation) + { + string cacheFile = context.GetServerCacheTargetFilePath(operation.NewAsset.AssetName); + + if (File.Exists(cacheFile)) + { + if (operation.NewAsset.AssetSize == new FileInfo(cacheFile).Length) + { + if (operation.NewAsset.AssetHashMd5.Equals(await MD5.HashFileAsync(cacheFile).ConfigureAwait(false), StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + + // Invalid file, delete it + File.Delete(cacheFile); + } + + string? directory = Path.GetDirectoryName(cacheFile); + ArgumentException.ThrowIfNullOrEmpty(directory); + Directory.CreateDirectory(directory); + + await EnsureAssetAsync(context, operation).ConfigureAwait(false); + } + + private async ValueTask EnsureAssetAsync(PackageConverterContext context, PackageItemOperationForSophonChunks asset) + { + if (asset.NewAsset.AssetType is 64) + { + return; + } + + IEnumerable chunks = asset.Kind switch + { + PackageItemOperationKind.Add => asset.NewAsset.AssetChunks.Select(chunk => new SophonChunk(asset.UrlPrefix, chunk)), + PackageItemOperationKind.Replace => asset.DiffChunks, + _ => [], + }; + + await DownloadChunksAsync(context, chunks).ConfigureAwait(false); + await MergeAssetAsync(context, asset).ConfigureAwait(false); + } + + private async ValueTask MergeAssetAsync(PackageConverterContext context, PackageItemOperationForSophonChunks asset) + { + ValueTask task = asset.Kind switch + { + PackageItemOperationKind.Add => MergeNewAssetAsync(context, asset.NewAsset), + PackageItemOperationKind.Replace => MergeDiffAssetAsync(context, asset), + _ => ValueTask.CompletedTask, + }; + + // TODO: Set Progress as indeterminate + await task.ConfigureAwait(false); + } + + private async ValueTask MergeDiffAssetAsync(PackageConverterContext context, PackageItemOperationForSophonChunks asset) + { + using (MemoryStream newAssetStream = memoryStreamFactory.GetStream()) + { + string oldAssetPath = Path.Combine(context.GameFileSystem.GameDirectory, asset.OldAsset.AssetName); + if (!File.Exists(oldAssetPath)) + { + // File not found, skip this asset and repair later + return; + } + + using (SafeFileHandle oldAssetHandle = File.OpenHandle(oldAssetPath, options: FileOptions.RandomAccess)) + { + foreach (AssetChunk chunk in asset.NewAsset.AssetChunks) + { + newAssetStream.Position = chunk.ChunkOnFileOffset; + + if (asset.OldAsset.AssetChunks.FirstOrDefault(c => c.ChunkDecompressedHashMd5 == chunk.ChunkDecompressedHashMd5) is not { } oldChunk) + { + string chunkPath = Path.Combine(context.ServerCacheChunksFolder, chunk.ChunkName); + if (!File.Exists(chunkPath)) + { + // File not found, skip this asset and repair later + return; + } + + using (await context.ExclusiveProcessChunkAsync(chunk.ChunkName).ConfigureAwait(false)) + { + using (FileStream diffStream = File.OpenRead(chunkPath)) + { + using (ZstandardDecompressionStream decompressor = new(diffStream)) + { + await decompressor.CopyToAsync(newAssetStream).ConfigureAwait(false); + } + } + + if (!context.DuplicatedChunkNames.ContainsKey(chunk.ChunkName)) + { + FileOperation.Delete(chunkPath); + } + } + } + else + { + using (IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(81920)) + { + Memory buffer = memoryOwner.Memory; + long offset = oldChunk.ChunkOnFileOffset; + long bytesToCopy = oldChunk.ChunkSizeDecompressed; + while (bytesToCopy > 0) + { + int bytesRead = await RandomAccess.ReadAsync(oldAssetHandle, buffer[..(int)Math.Min(buffer.Length, bytesToCopy)], offset).ConfigureAwait(false); + if (bytesRead <= 0) + { + break; + } + + await newAssetStream.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false); + offset += bytesRead; + bytesToCopy -= bytesRead; + } + } + } + } + } + + string path = context.GetServerCacheTargetFilePath(asset.NewAsset.AssetName); + using (FileStream newAssetFileStream = File.Create(path)) + { + newAssetStream.Position = 0; + await newAssetStream.CopyToAsync(newAssetFileStream).ConfigureAwait(false); + } + } + } + + private bool ReplaceGameResource(PackageConverterContext context, List operations) + { + // 执行下载与移动操作 + foreach (PackageItemOperationForSophonChunks operation in operations) + { + (bool moveToBackup, bool moveToTarget) = operation.Kind switch + { + PackageItemOperationKind.Backup => (true, false), + PackageItemOperationKind.Replace => (true, true), + PackageItemOperationKind.Add => (false, true), + _ => (false, false), + }; + + // 先备份 + if (moveToBackup) + { + string localFileName = operation.OldAsset.AssetName; + context.Progress.Report(new(SH.FormatServiceGamePackageConvertMoveFileBackupFormat(localFileName))); + + string localFilePath = context.GetGameFolderFilePath(localFileName); + string cacheFilePath = context.GetServerCacheBackupFilePath(localFileName); + string? cacheFileDirectory = Path.GetDirectoryName(cacheFilePath); + ArgumentException.ThrowIfNullOrEmpty(cacheFileDirectory); + Directory.CreateDirectory(cacheFileDirectory); + + logger.LogInformation("Backing file from:{Src} to:{Dst}", localFilePath, cacheFilePath); + FileOperation.Move(localFilePath, cacheFilePath, true); + } + + // 后替换 + if (moveToTarget) + { + string targetFileName = operation.NewAsset.AssetName; + context.Progress.Report(new(SH.FormatServiceGamePackageConvertMoveFileRestoreFormat(targetFileName))); + + string targetFilePath = context.GetGameFolderFilePath(targetFileName); + string? targetFileDirectory = Path.GetDirectoryName(targetFilePath); + string cacheFilePath = context.GetServerCacheTargetFilePath(targetFileName); + ArgumentException.ThrowIfNullOrEmpty(targetFileDirectory); + Directory.CreateDirectory(targetFileDirectory); + + logger.LogInformation("Restoring file from:{Src} to:{Dst}", cacheFilePath, targetFilePath); + FileOperation.Move(cacheFilePath, targetFilePath, true); + } + } + + // 重命名 _Data 目录 + try + { + context.Progress.Report(new(SH.FormatServiceGamePackageConvertMoveFileRenameFormat(context.FromDataFolderName, context.ToDataFolderName))); + DirectoryOperation.Move(context.FromDataFolder, context.ToDataFolder); + } + catch (IOException ex) + { + // Access to the path is denied. + // When user install the game in special folder like 'Program Files' + throw HutaoException.Throw(SH.ServiceGamePackageRenameDataFolderFailed, ex); + } + + return true; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs index a508c9d7b5..a2fa530620 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs @@ -3,27 +3,14 @@ namespace Snap.Hutao.Service.Game.Package; -/// -/// 包版本项 -/// -[HighQuality] internal sealed class VersionItem { - /// - /// 服务器上的名称 - /// [JsonPropertyName("remoteName")] public string RelativePath { get; set; } = default!; - /// - /// MD5校验值 - /// [JsonPropertyName("md5")] public string Md5 { get; set; } = default!; - /// - /// 文件尺寸 - /// [JsonPropertyName("fileSize")] public long FileSize { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs index e35eab333c..04af2c9d8a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs @@ -29,7 +29,7 @@ public async ValueTask> SilentGetGamePathAsync() if (isOk) { // Save result. - launchOptions.UpdateGamePathAndRefreshEntries(path); + launchOptions.UpdateGamePath(path); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs index 7932c633bf..c4e9b75380 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -28,7 +28,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker private readonly string dataFolderIslandPath; private readonly string gameVersion; - private IslandFunctionOffsets? offsets; + private IslandFunctionOffsets offsets; private int accumulatedBadStateCount; public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, string gameVersion) @@ -93,11 +93,10 @@ public async ValueTask PostUnlockAsync(CancellationToken token = default) } IslandEnvironmentView view = UpdateIslandEnvironment(handle, launchOptions); - context.Logger.LogDebug("Island Environment|{State}|{Error}|{Value}", view.State, view.LastError, view.DebugOriginalFieldOfView); if (view.State is IslandState.None or IslandState.Stopped) { - if (Interlocked.Increment(ref accumulatedBadStateCount) >= 5) + if (Interlocked.Increment(ref accumulatedBadStateCount) >= 10) { HutaoException.Throw($"UnlockerIsland in bad state for too long, last state: {view.State}"); } @@ -120,11 +119,11 @@ public async ValueTask PostUnlockAsync(CancellationToken token = default) private static unsafe void InitializeIslandEnvironment(nint handle, IslandFunctionOffsets offsets, LaunchOptions options) { IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle; - pIslandEnvironment->FunctionOffsetSetFieldOfView = offsets.FunctionOffsetSetFieldOfView; - pIslandEnvironment->FunctionOffsetSetTargetFrameRate = offsets.FunctionOffsetSetTargetFrameRate; - pIslandEnvironment->FunctionOffsetSetEnableFogRendering = offsets.FunctionOffsetSetEnableFogRendering; - pIslandEnvironment->LoopAdjustFpsOnly = options.LoopAdjustFpsOnly; + pIslandEnvironment->FunctionOffsets = offsets; + pIslandEnvironment->HookingSetFieldOfView = options.HookingSetFieldOfView; + pIslandEnvironment->HookingOpenTeam = options.HookingOpenTeam; + pIslandEnvironment->HookingMickyWonderPartner2 = options.HookingMickyWonderPartner2; UpdateIslandEnvironment(handle, options); } @@ -132,9 +131,14 @@ private static unsafe void InitializeIslandEnvironment(nint handle, IslandFuncti private static unsafe IslandEnvironmentView UpdateIslandEnvironment(nint handle, LaunchOptions options) { IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle; + + pIslandEnvironment->EnableSetFieldOfView = options.IsSetFieldOfViewEnabled; pIslandEnvironment->FieldOfView = options.TargetFov; - pIslandEnvironment->TargetFrameRate = options.TargetFps; + pIslandEnvironment->FixLowFovScene = options.FixLowFovScene; pIslandEnvironment->DisableFog = options.DisableFog; + pIslandEnvironment->EnableSetTargetFrameRate = options.IsSetTargetFrameRateEnabled; + pIslandEnvironment->TargetFrameRate = options.TargetFps; + pIslandEnvironment->RemoveOpenTeamProgress = options.RemoveOpenTeamProgress; return *(IslandEnvironmentView*)pIslandEnvironment; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironment.cs index d52917a59d..1e6d4b2de0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironment.cs @@ -10,14 +10,16 @@ internal struct IslandEnvironment public IslandState State; public WIN32_ERROR LastError; + public IslandFunctionOffsets FunctionOffsets; + + public BOOL HookingSetFieldOfView; + public BOOL EnableSetFieldOfView; public float FieldOfView; + public BOOL FixLowFovScene; + public BOOL DisableFog; + public BOOL EnableSetTargetFrameRate; public int TargetFrameRate; - public bool DisableFog; - public bool LoopAdjustFpsOnly; - - public uint FunctionOffsetSetFieldOfView; - public uint FunctionOffsetSetEnableFogRendering; - public uint FunctionOffsetSetTargetFrameRate; - - public float DebugOriginalFieldOfView; + public BOOL HookingOpenTeam; + public BOOL RemoveOpenTeamProgress; + public BOOL HookingMickyWonderPartner2; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironmentView.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironmentView.cs index 8f39600db7..1b0c3f6bee 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironmentView.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandEnvironmentView.cs @@ -5,19 +5,8 @@ namespace Snap.Hutao.Service.Game.Unlocker.Island; -internal struct IslandEnvironmentView +internal readonly struct IslandEnvironmentView { - public IslandState State; - public WIN32_ERROR LastError; - - public float FieldOfView; - public int TargetFrameRate; - public bool DisableFog; - public bool LoopAdjustFpsOnly; - - public uint FunctionOffsetSetFieldOfView; - public uint FunctionOffsetSetEnableFogRendering; - public uint FunctionOffsetSetTargetFrameRate; - - public float DebugOriginalFieldOfView; + public readonly IslandState State; + public readonly WIN32_ERROR LastError; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandFunctionOffsets.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandFunctionOffsets.cs index eee03fc2b0..94ab47e43b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandFunctionOffsets.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/Island/IslandFunctionOffsets.cs @@ -3,11 +3,22 @@ namespace Snap.Hutao.Service.Game.Unlocker.Island; -internal sealed class IslandFunctionOffsets +internal struct IslandFunctionOffsets { - public required uint FunctionOffsetSetFieldOfView { get; set; } - - public required uint FunctionOffsetSetEnableFogRendering { get; set; } - - public required uint FunctionOffsetSetTargetFrameRate { get; set; } + [JsonInclude] + public uint FunctionOffsetMickeyWonderMethod; + [JsonInclude] + public uint FunctionOffsetMickeyWonderMethodPartner; + [JsonInclude] + public uint FunctionOffsetMickeyWonderMethodPartner2; + [JsonInclude] + public uint FunctionOffsetSetFieldOfView; + [JsonInclude] + public uint FunctionOffsetSetEnableFogRendering; + [JsonInclude] + public uint FunctionOffsetSetTargetFrameRate; + [JsonInclude] + public uint FunctionOffsetOpenTeam; + [JsonInclude] + public uint FunctionOffsetOpenTeamPageAccordingly; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestService.cs index dbf387826e..073676a641 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestService.cs @@ -24,7 +24,7 @@ internal sealed partial class GeetestService : IGeetestService private readonly ITaskContext taskContext; private readonly CardClient cardClient; - public async ValueTask TryVerifyGtChallengeAsync(string gt, string challenge, CancellationToken token = default) + public async ValueTask TryVerifyGtChallengeAsync(string gt, string challenge, bool isOversea, CancellationToken token = default) { GeetestResponse response = await customGeetestClient.VerifyAsync(gt, challenge, token).ConfigureAwait(false); @@ -33,7 +33,7 @@ internal sealed partial class GeetestService : IGeetestService return data; } - string? result = await PrivateVerifyByWebViewAsync(gt, challenge, false, token).ConfigureAwait(false); + string? result = await PrivateVerifyByWebViewAsync(gt, challenge, isOversea, token).ConfigureAwait(false); if (string.IsNullOrEmpty(result)) { return default; @@ -52,13 +52,13 @@ internal sealed partial class GeetestService : IGeetestService public async ValueTask TryVerifyXrpcChallengeAsync(Model.Entity.User user, CardVerifiationHeaders headers, CancellationToken token = default) { - Response registrationResponse = await cardClient.CreateVerificationAsync(user, headers, token).ConfigureAwait(false); - if (!ResponseValidator.TryValidate(registrationResponse, infoBarService, out VerificationRegistration? registration)) + Response registrationResponse = await cardClient.CreateVerificationAsync(user, headers, token).ConfigureAwait(false); + if (!ResponseValidator.TryValidate(registrationResponse, infoBarService, out GeetestVerification? registration)) { return default; } - if (await TryVerifyGtChallengeAsync(registration.Gt, registration.Challenge, token).ConfigureAwait(false) is not { } data) + if (await TryVerifyGtChallengeAsync(registration.Gt, registration.Challenge, user.IsOversea, token).ConfigureAwait(false) is not { } data) { return default; } @@ -82,18 +82,18 @@ public async ValueTask TryVerifyAigisSessionAsync(IAigisProvider provider, AigisSession? session = JsonSerializer.Deserialize(rawSession); ArgumentNullException.ThrowIfNull(session); - AigisData? sessionData = JsonSerializer.Deserialize(session.Data); - ArgumentNullException.ThrowIfNull(sessionData); + GeetestVerification? verification = JsonSerializer.Deserialize(session.Data); + ArgumentNullException.ThrowIfNull(verification); - string? result = await PrivateVerifyByWebViewAsync(sessionData.GT, sessionData.Challenge, isOversea, token).ConfigureAwait(false); - - if (string.IsNullOrEmpty(result)) + if (await TryVerifyGtChallengeAsync(verification.Gt, verification.Challenge, isOversea, token).ConfigureAwait(false) is not { } data) { - // User closed the window without completing the verification + // Custom Geetest failed and user closed the window without completing the verification return false; } - provider.Aigis = $"{session.SessionId};{Convert.ToBase64String(Encoding.UTF8.GetBytes(result))}"; + GeetestWebResponse result = new(data.Challenge, data.Validate); + + provider.Aigis = $"{session.SessionId};{Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)))}"; return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/AigisData.cs b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestVerification.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Service/Geetest/AigisData.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestVerification.cs index 5904898b7c..8c6d44a846 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/AigisData.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestVerification.cs @@ -3,13 +3,13 @@ namespace Snap.Hutao.Service.Geetest; -internal sealed class AigisData +internal sealed class GeetestVerification { [JsonPropertyName("success")] public int Success { get; set; } [JsonPropertyName("gt")] - public string GT { get; set; } = default!; + public string Gt { get; set; } = default!; [JsonPropertyName("challenge")] public string Challenge { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestWebResponse.cs b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestWebResponse.cs index 429f5b7c7f..ce79020a6f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestWebResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/GeetestWebResponse.cs @@ -5,12 +5,19 @@ namespace Snap.Hutao.Service.Geetest; internal sealed class GeetestWebResponse { + public GeetestWebResponse(string challenge, string validate) + { + Challenge = challenge; + Validate = validate; + Seccode = $"{validate}|jordan"; + } + [JsonPropertyName("geetest_challenge")] - public string Challenge { get; set; } = default!; + public string Challenge { get; set; } [JsonPropertyName("geetest_validate")] - public string Validate { get; set; } = default!; + public string Validate { get; set; } [JsonPropertyName("geetest_seccode")] - public string Seccode { get; set; } = default!; + public string Seccode { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/IGeetestService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/IGeetestService.cs index 87ea025637..87a9f92b5b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Geetest/IGeetestService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Geetest/IGeetestService.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.Geetest; internal interface IGeetestService { - ValueTask TryVerifyGtChallengeAsync(string gt, string challenge, CancellationToken token = default); + ValueTask TryVerifyGtChallengeAsync(string gt, string challenge, bool isOversea, CancellationToken token = default); ValueTask TryVerifyXrpcChallengeAsync(Model.Entity.User user, CardVerifiationHeaders headers, CancellationToken token = default); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs index b6b3edd542..7cb9547f95 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs @@ -42,7 +42,11 @@ public async ValueTask> GetHutaoAnnounce } } - list.ForEach(item => item.DismissCommand = dismissCommand); + foreach (HutaoAnnouncement item in list) + { + item.DismissCommand = dismissCommand; + } + announcements = list.ToObservableCollection(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index 3485c3fa30..121466f421 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -202,13 +202,13 @@ private Task CheckMetadataSourceFilesAsync(ImmutableDictionary m bool skip = false; if (File.Exists(fileFullPath)) { - skip = hash == await XXH64.HashFileAsync(fileFullPath, token).ConfigureAwait(false); + skip = hash == await XXH64.HashFileAsync(fileFullPath, token).ConfigureAwait(true); } if (!skip) { logger.LogInformation("{Hash} of {File} not matched, begin downloading", nameof(XXH64), fileFullName); - await DownloadMetadataSourceFilesAsync(fileFullName, token).ConfigureAwait(false); + await DownloadMetadataSourceFilesAsync(fileFullName, token).ConfigureAwait(true); } }); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index a8ceff2500..b9423cbfef 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -1,8 +1,10 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.DependencyInjection.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.ViewModel.User; using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Passport; @@ -19,6 +21,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe { private readonly IProfilePictureService profilePictureService; private readonly IUserCollectionService userCollectionService; + private readonly IContentDialogFactory contentDialogFactory; private readonly IServiceProvider serviceProvider; private readonly IUserRepository userRepository; private readonly ITaskContext taskContext; @@ -41,34 +44,40 @@ public ValueTask> GetUsersAsyn public async ValueTask> ProcessInputCookieAsync(InputCookie inputCookie) { - await taskContext.SwitchToBackgroundAsync(); - (Cookie cookie, bool _, string? deviceFp) = inputCookie; - - string? mid = cookie.GetValueOrDefault(Cookie.MID); + ContentDialog dialog = await contentDialogFactory + .CreateForIndeterminateProgressAsync(SH.ServiceUserProcessInputCookieDialogTitle) + .ConfigureAwait(false); - if (string.IsNullOrEmpty(mid)) + using (await contentDialogFactory.BlockAsync(dialog).ConfigureAwait(false)) { - return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoMid); - } + await taskContext.SwitchToBackgroundAsync(); + (Cookie cookie, bool _, string? deviceFp) = inputCookie; - // 检查 mid 对应用户是否存在 - if (await this.GetUserByMidAsync(mid).ConfigureAwait(false) is not { } user) - { - return await userCollectionService.TryCreateAndAddUserFromInputCookieAsync(inputCookie).ConfigureAwait(false); - } + string? mid = cookie.GetValueOrDefault(Cookie.MID); - if (!cookie.TryGetSToken(out Cookie? stoken)) - { - return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoSToken); - } + if (string.IsNullOrEmpty(mid)) + { + return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoMid); + } + + if (await this.GetUserByMidAsync(mid).ConfigureAwait(false) is not { } user) + { + return await userCollectionService.TryCreateAndAddUserFromInputCookieAsync(inputCookie).ConfigureAwait(false); + } - user.SToken = stoken; - user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; - user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; - user.TryUpdateFingerprint(deviceFp); + if (!cookie.TryGetSToken(out Cookie? stoken)) + { + return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoSToken); + } - userRepository.UpdateUser(user.Entity); - return new(UserOptionResult.CookieUpdated, mid); + user.SToken = stoken; + user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken; + user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; + user.TryUpdateFingerprint(deviceFp); + + userRepository.UpdateUser(user.Entity); + return new(UserOptionResult.CookieUpdated, mid); + } } public async ValueTask RefreshCookieTokenAsync(EntityUser user) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs index 2f4e25be9f..bc9778621f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserServiceExtension.cs @@ -19,18 +19,7 @@ public static ValueTask RefreshCookieTokenAsync(this IUserService userServ public static async ValueTask GetUserGameRoleByUidAsync(this IUserService userService, string uid) { AdvancedDbCollectionView users = await userService.GetUsersAsync().ConfigureAwait(false); - foreach (BindingUser user in users.SourceCollection) - { - foreach (UserGameRole role in user.UserGameRoles) - { - if (role.GameUid == uid) - { - return role; - } - } - } - - return null; + return users.SourceCollection.SelectMany(user => user.UserGameRoles.SourceCollection).FirstOrDefault(role => role.GameUid == uid); } public static async ValueTask GetCurrentUidAsync(this IUserService userService) diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 4ed9c48d8e..de8a48dd66 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -87,6 +87,7 @@ + @@ -100,6 +101,8 @@ + + @@ -194,6 +197,7 @@ + @@ -292,6 +296,8 @@ + + @@ -314,7 +320,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -338,9 +348,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -348,11 +358,11 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Shell/NotifyIconXamlHostWindow.cs b/src/Snap.Hutao/Snap.Hutao/UI/Shell/NotifyIconXamlHostWindow.cs index a32f3b28d2..9b0d91403d 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Shell/NotifyIconXamlHostWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/UI/Shell/NotifyIconXamlHostWindow.cs @@ -22,8 +22,8 @@ public NotifyIconXamlHostWindow(IServiceProvider serviceProvider) { Content = new Border(); - this.SetExStyleLayered(); - this.SetExStyleToolWindow(); + this.AddExStyleLayered(); + this.AddExStyleToolWindow(); AppWindow.Title = "SnapHutaoNotifyIconXamlHost"; AppWindow.IsShownInSwitchers = false; diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Windowing/Abstraction/IXamlWindowRectPersisted.cs b/src/Snap.Hutao/Snap.Hutao/UI/Windowing/Abstraction/IXamlWindowRectPersisted.cs index 4fc1e65bd1..8b84d66bea 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Windowing/Abstraction/IXamlWindowRectPersisted.cs +++ b/src/Snap.Hutao/Snap.Hutao/UI/Windowing/Abstraction/IXamlWindowRectPersisted.cs @@ -6,4 +6,6 @@ namespace Snap.Hutao.UI.Windowing.Abstraction; internal interface IXamlWindowRectPersisted : IXamlWindowHasInitSize { string PersistRectKey { get; } + + string PersistScaleKey { get; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Windowing/XamlWindowController.cs b/src/Snap.Hutao/Snap.Hutao/UI/Windowing/XamlWindowController.cs index ebc1f378a5..47ffc51777 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Windowing/XamlWindowController.cs +++ b/src/Snap.Hutao/Snap.Hutao/UI/Windowing/XamlWindowController.cs @@ -260,8 +260,7 @@ private void RecoverOrInitWindowSize(IXamlWindowHasInitSize xamlWindow) if (window is IXamlWindowRectPersisted rectPersisted) { RectInt32 nonDpiPersistedRect = (RectInt16)LocalSetting.Get(rectPersisted.PersistRectKey, (RectInt16)rect); - window.AppWindow.Move(nonDpiPersistedRect.GetPointInt32(PointInt32Kind.TopLeft)); - RectInt32 persistedRect = nonDpiPersistedRect.Scale(window.GetRasterizationScale()); + RectInt32 persistedRect = nonDpiPersistedRect.Scale(LocalSetting.Get(rectPersisted.PersistScaleKey, 1.0)); // If the persisted size is less than min size, we want to reset to the init size. // So we only recover the size when it's greater than or equal to the min size. @@ -281,12 +280,15 @@ private void SaveOrSkipWindowSize(IXamlWindowRectPersisted rectPersisted) GetWindowPlacement(window.GetWindowHandle(), ref windowPlacement); // prevent save value when we are maximized. - if (!windowPlacement.ShowCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED)) + if (windowPlacement.ShowCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED)) { - // We save the non-dpi rect here - double scale = 1.0 / window.GetRasterizationScale(); - LocalSetting.Set(rectPersisted.PersistRectKey, (RectInt16)window.AppWindow.GetRect().Scale(scale)); + return; } + + // We save the non-dpi rect here + double scale = window.GetRasterizationScale(); + LocalSetting.Set(rectPersisted.PersistScaleKey, scale); + LocalSetting.Set(rectPersisted.PersistRectKey, (RectInt16)window.AppWindow.GetRect().Scale(1.0 / scale)); } #endregion diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Behavior/PeriodicInvokeCommandOrOnActualThemeChangedBehavior.cs b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Behavior/PeriodicInvokeCommandOrOnActualThemeChangedBehavior.cs index 62c544cc3b..20b519fd98 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Behavior/PeriodicInvokeCommandOrOnActualThemeChangedBehavior.cs +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Behavior/PeriodicInvokeCommandOrOnActualThemeChangedBehavior.cs @@ -16,8 +16,6 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio private CancellationTokenSource acutalThemeChangedCts = new(); private CancellationTokenSource periodicTimerStopCts = new(); - private bool shouldReactToActualThemeChange; - protected override bool Initialize() { AssociatedObject.ActualThemeChanged += OnActualThemeChanged; @@ -42,10 +40,7 @@ protected override void OnAssociatedObjectLoaded() private void OnActualThemeChanged(FrameworkElement sender, object args) { - if (shouldReactToActualThemeChange) - { - acutalThemeChangedCts.Cancel(); - } + acutalThemeChangedCts.Cancel(); } private void TryExecuteCommand() @@ -70,17 +65,14 @@ private async Task RunCoreAsync() break; } - // TODO: Reconsider approach to get the ServiceProvider - ITaskContext taskContext = Ioc.Default.GetRequiredService(); - await taskContext.SwitchToMainThreadAsync(); - TryExecuteCommand(); - + ITaskContext taskContext = TaskContext.GetForDispatcherQueue(AssociatedObject.DispatcherQueue); await taskContext.SwitchToBackgroundAsync(); try { using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(acutalThemeChangedCts.Token, periodicTimerStopCts.Token)) { await timer.WaitForNextTickAsync(linkedCts.Token).ConfigureAwait(false); + taskContext.BeginInvokeOnMainThread(TryExecuteCommand); } } catch (OperationCanceledException) @@ -91,8 +83,6 @@ private async Task RunCoreAsync() } } - shouldReactToActualThemeChange = true; - acutalThemeChangedCts.Dispose(); acutalThemeChangedCts = new(); periodicTimerStopCts.Dispose(); diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ContentDialogExtension.cs b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ContentDialogExtension.cs deleted file mode 100644 index 37bf27bfc1..0000000000 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ContentDialogExtension.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Snap.Hutao.Factory.ContentDialog; - -namespace Snap.Hutao.UI.Xaml.Control; - -internal static class ContentDialogExtension -{ - public static async ValueTask BlockAsync(this ContentDialog contentDialog, IContentDialogFactory contentDialogFactory) - { - TaskCompletionSource dialogShowSource = new(); - _ = contentDialogFactory.EnqueueAndShowAsync(contentDialog, dialogShowSource); - contentDialog.DispatcherQueue.TryEnqueue(() => contentDialog.Focus(FocusState.Programmatic)); - await dialogShowSource.Task.ConfigureAwait(false); - return new ContentDialogScope(contentDialog, contentDialogFactory.TaskContext); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ContentDialogScope.cs b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ContentDialogScope.cs deleted file mode 100644 index c80c5642ef..0000000000 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ContentDialogScope.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.UI.Xaml.Controls; - -namespace Snap.Hutao.UI.Xaml.Control; - -internal struct ContentDialogScope : IDisposable, IAsyncDisposable -{ - private readonly ContentDialog contentDialog; - private readonly ITaskContext taskContext; - - private bool disposing = false; - private bool disposed = false; - - public ContentDialogScope(ContentDialog contentDialog, ITaskContext taskContext) - { - this.contentDialog = contentDialog; - this.taskContext = taskContext; - } - - public void Dispose() - { - if (!disposed && !disposing) - { - disposing = true; - taskContext.InvokeOnMainThread(contentDialog.Hide); - disposing = false; - disposed = true; - } - } - - public async ValueTask DisposeAsync() - { - if (!disposed && !disposing) - { - disposing = true; - await taskContext.SwitchToMainThreadAsync(); - contentDialog.Hide(); - disposing = false; - disposed = true; - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ControlHelper.cs b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ControlHelper.cs new file mode 100644 index 0000000000..169a3fe8d4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ControlHelper.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; + +namespace Snap.Hutao.UI.Xaml.Control; + +[SuppressMessage("", "SH001")] +[DependencyProperty("IsDisabled1", typeof(bool), default(bool), nameof(OnIsDisabledChanged), IsAttached = true, AttachedType = typeof(Microsoft.UI.Xaml.Controls.Control))] +[DependencyProperty("IsDisabled2", typeof(bool), default(bool), nameof(OnIsDisabledChanged), IsAttached = true, AttachedType = typeof(Microsoft.UI.Xaml.Controls.Control))] +[DependencyProperty("IsDisabled3", typeof(bool), default(bool), nameof(OnIsDisabledChanged), IsAttached = true, AttachedType = typeof(Microsoft.UI.Xaml.Controls.Control))] +public sealed partial class ControlHelper +{ + private static void OnIsDisabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + if (sender is not Microsoft.UI.Xaml.Controls.Control control) + { + return; + } + + control.IsEnabled = !GetAnyDisabled(control); + } + + private static bool GetAnyDisabled(Microsoft.UI.Xaml.Controls.Control control) + { + return GetIsDisabled1(control) || GetIsDisabled2(control) || GetIsDisabled3(control); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.cs b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.cs index 4ab84c6244..ab448e7c43 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.cs +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.cs @@ -7,5 +7,6 @@ namespace Snap.Hutao.UI.Xaml.Control; [DependencyProperty("Quality", typeof(QualityType), QualityType.QUALITY_NONE)] [DependencyProperty("Icon", typeof(Uri))] +[DependencyProperty("IconOpacity", typeof(double), 1.0)] [DependencyProperty("Badge", typeof(Uri))] internal sealed partial class ItemIcon : Microsoft.UI.Xaml.Controls.Control; \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.xaml index 44b583e09b..f67c74f3c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.xaml +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/Control/ItemIcon.xaml @@ -19,7 +19,7 @@ - + + + + +