From 13ad36f5b4842e11d8caca47817527815b1ad237 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Thu, 2 Nov 2023 18:43:07 +0800 Subject: [PATCH 01/24] Added a master switch for launchOptions --- .../Model/Entity/SettingEntry.Constant.cs | 5 ++++ .../Snap.Hutao/Service/Game/LaunchOptions.cs | 10 ++++++++ .../Snap.Hutao/Service/Game/ProcessInterop.cs | 24 ++++++++++++------- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 13 +++++----- 4 files changed, 38 insertions(+), 14 deletions(-) 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 5c96234d..4c0b1799 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -53,6 +53,11 @@ internal sealed partial class SettingEntry /// public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl"; + /// + /// 启动游戏 总开关 + /// + public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled"; + /// /// 启动游戏 独占全屏 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index c086cd15..3d62496b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -23,6 +23,7 @@ internal sealed class LaunchOptions : DbStoreOptions private readonly int primaryScreenHeight; private readonly int primaryScreenFps; + private bool? isLaunchOptionsEnabled; private bool? isFullScreen; private bool? isBorderless; private bool? isExclusive; @@ -50,6 +51,15 @@ internal sealed class LaunchOptions : DbStoreOptions InitializeScreenFps(out primaryScreenFps); } + /// + /// 是否启用启动参数 + /// + public bool IsLaunchOptionsEnabled + { + get => GetOption(ref isLaunchOptionsEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, false); + set => SetOption(ref isLaunchOptionsEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); + } + /// /// 是否全屏 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs index 15aafcd4..683bc5bd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs @@ -30,14 +30,22 @@ internal static class ProcessInterop // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html - string commandLine = new CommandLineBuilder() - .AppendIf("-popupwindow", options.IsBorderless) - .AppendIf("-window-mode", options.IsExclusive, "exclusive") - .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) - .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) - .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) - .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) - .ToString(); + string commandLine; + if (options.IsLaunchOptionsEnabled) + { + commandLine = new CommandLineBuilder() + .AppendIf("-popupwindow", options.IsBorderless) + .AppendIf("-window-mode", options.IsExclusive, "exclusive") + .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) + .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) + .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) + .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) + .ToString(); + } + else + { + commandLine = string.Empty; + } return new() { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index fe5be18d..6a65bbac 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -199,17 +199,18 @@ Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}" HeaderIcon="{shcm:FontIcon Glyph=}" IsExpanded="True"> + - + - + - + - + - + - + Date: Thu, 2 Nov 2023 21:14:39 +0800 Subject: [PATCH 02/24] code style --- .../Snap.Hutao/Service/Game/LaunchOptions.cs | 8 ++--- .../Snap.Hutao/Service/Game/ProcessInterop.cs | 15 ++++----- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 33 ++++++++++++++----- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 3d62496b..ac514a31 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -23,7 +23,7 @@ internal sealed class LaunchOptions : DbStoreOptions private readonly int primaryScreenHeight; private readonly int primaryScreenFps; - private bool? isLaunchOptionsEnabled; + private bool? isEnabled; private bool? isFullScreen; private bool? isBorderless; private bool? isExclusive; @@ -54,10 +54,10 @@ internal sealed class LaunchOptions : DbStoreOptions /// /// 是否启用启动参数 /// - public bool IsLaunchOptionsEnabled + public bool IsEnabled { - get => GetOption(ref isLaunchOptionsEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, false); - set => SetOption(ref isLaunchOptionsEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); + get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true); + set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs index 683bc5bd..3b3dc973 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs @@ -26,13 +26,14 @@ internal static class ProcessInterop /// 初始化后的游戏进程 public static Process InitializeGameProcess(LaunchOptions options, string gamePath) { - Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); + string commandLine = string.Empty; - // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html - // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html - string commandLine; - if (options.IsLaunchOptionsEnabled) + if (options.IsEnabled) { + Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); + + // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html + // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html commandLine = new CommandLineBuilder() .AppendIf("-popupwindow", options.IsBorderless) .AppendIf("-window-mode", options.IsExclusive, "exclusive") @@ -42,10 +43,6 @@ internal static class ProcessInterop .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) .ToString(); } - else - { - commandLine = string.Empty; - } return new() { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 6a65bbac..fa9098e9 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -192,25 +192,36 @@ - - + - + - + - + - + - + - + Date: Fri, 3 Nov 2023 09:26:16 +0800 Subject: [PATCH 03/24] fix pushpage --- .../Web/Bridge/MiHoYoJSInterface.cs | 211 ++++++++++-------- 1 file changed, 114 insertions(+), 97 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs index 75a127a8..644a0f7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs @@ -74,6 +74,36 @@ internal class MiHoYoJSInterface public event Action? ClosePageRequested; + /// + /// 关闭 + /// + /// 参数 + /// 响应 + public virtual async ValueTask ClosePageAsync(JsParam param) + { + await taskContext.SwitchToMainThreadAsync(); + if (coreWebView2.CanGoBack) + { + coreWebView2.GoBack(); + } + else + { + ClosePageRequested?.Invoke(); + } + + return null; + } + + /// + /// 调整分享设置 + /// + /// 参数 + /// 响应 + public virtual IJsResult? ConfigureShare(JsParam param) + { + return null; + } + /// /// 获取ActionTicket /// @@ -87,24 +117,6 @@ internal class MiHoYoJSInterface .ConfigureAwait(false); } - /// - /// 获取Http请求头 - /// - /// 参数 - /// Http请求头 - public virtual JsResult> GetHttpRequestHeader(JsParam param) - { - return new() - { - Data = new Dictionary() - { - { "x-rpc-client_type", "5" }, - { "x-rpc-device_id", HoyolabOptions.DeviceId }, - { "x-rpc-app_version", SaltConstants.CNVersion }, - }, - }; - } - /// /// 异步获取账户信息 /// @@ -125,6 +137,45 @@ internal class MiHoYoJSInterface }; } + /// + /// 获取CookieToken + /// + /// 参数 + /// 响应 + public virtual async ValueTask>> GetCookieTokenAsync(JsParam param) + { + IUserService userService = serviceProvider.GetRequiredService(); + if (param.Payload.ForceRefresh) + { + await userService.RefreshCookieTokenAsync(userAndUid.User).ConfigureAwait(false); + } + + await taskContext.SwitchToMainThreadAsync(); + coreWebView2.SetCookie(userAndUid.User.CookieToken, userAndUid.User.LToken); + + ArgumentNullException.ThrowIfNull(userAndUid.User.CookieToken); + return new() { Data = new() { [Cookie.COOKIE_TOKEN] = userAndUid.User.CookieToken[Cookie.COOKIE_TOKEN] } }; + } + + /// + /// 获取当前语言和时区 + /// + /// param + /// 语言与时区 + public virtual JsResult> GetCurrentLocale(JsParam param) + { + MetadataOptions metadataOptions = serviceProvider.GetRequiredService(); + + return new() + { + Data = new() + { + ["language"] = metadataOptions.LanguageCode, + ["timeZone"] = "GMT+8", + }, + }; + } + /// /// 获取1代动态密钥 /// @@ -179,6 +230,34 @@ internal class MiHoYoJSInterface } } + /// + /// 获取Http请求头 + /// + /// 参数 + /// Http请求头 + public virtual JsResult> GetHttpRequestHeader(JsParam param) + { + return new() + { + Data = new Dictionary() + { + { "x-rpc-client_type", "5" }, + { "x-rpc-device_id", HoyolabOptions.DeviceId }, + { "x-rpc-app_version", SaltConstants.CNVersion }, + }, + }; + } + + /// + /// 获取状态栏高度 + /// + /// 参数 + /// 结果 + public virtual JsResult> GetStatusBarHeight(JsParam param) + { + return new() { Data = new() { ["statusBarHeight"] = 0 } }; + } + /// /// 获取用户基本信息 /// @@ -213,90 +292,28 @@ internal class MiHoYoJSInterface } } - /// - /// 获取CookieToken - /// - /// 参数 - /// 响应 - public virtual async ValueTask>> GetCookieTokenAsync(JsParam param) - { - IUserService userService = serviceProvider.GetRequiredService(); - if (param.Payload.ForceRefresh) - { - await userService.RefreshCookieTokenAsync(userAndUid.User).ConfigureAwait(false); - } - - await taskContext.SwitchToMainThreadAsync(); - coreWebView2.SetCookie(userAndUid.User.CookieToken, userAndUid.User.LToken); - - ArgumentNullException.ThrowIfNull(userAndUid.User.CookieToken); - return new() { Data = new() { [Cookie.COOKIE_TOKEN] = userAndUid.User.CookieToken[Cookie.COOKIE_TOKEN] } }; - } - - /// - /// 关闭 - /// - /// 参数 - /// 响应 - public virtual async ValueTask ClosePageAsync(JsParam param) - { - await taskContext.SwitchToMainThreadAsync(); - if (coreWebView2.CanGoBack) - { - coreWebView2.GoBack(); - } - else - { - ClosePageRequested?.Invoke(); - } - - return null; - } - - /// - /// 调整分享设置 - /// - /// 参数 - /// 响应 - public virtual IJsResult? ConfigureShare(JsParam param) - { - return null; - } - - /// - /// 获取状态栏高度 - /// - /// 参数 - /// 结果 - public virtual JsResult> GetStatusBarHeight(JsParam param) - { - return new() { Data = new() { ["statusBarHeight"] = 0 } }; - } - public virtual async ValueTask PushPageAsync(JsParam param) { - await taskContext.SwitchToMainThreadAsync(); - coreWebView2.Navigate(param.Payload.Page); - return null; - } + const string bbsSchema = "mihoyobbs://"; + string pageUrl = param.Payload.Page; - /// - /// 获取当前语言和时区 - /// - /// param - /// 语言与时区 - public virtual JsResult> GetCurrentLocale(JsParam param) - { - MetadataOptions metadataOptions = serviceProvider.GetRequiredService(); - - return new() + string targetUrl = pageUrl; + if (pageUrl.AsSpan().StartsWith(bbsSchema, StringComparison.OrdinalIgnoreCase)) { - Data = new() + if (pageUrl.AsSpan(bbsSchema.Length).StartsWith("article/")) { - ["language"] = metadataOptions.LanguageCode, - ["timeZone"] = "GMT+8", - }, - }; + targetUrl = pageUrl.Replace("mihoyobbs://article/", "https://m.miyoushe.com/ys/#/article/", StringComparison.OrdinalIgnoreCase); + } + else if (pageUrl.AsSpan(bbsSchema.Length).StartsWith("webview?link=")) + { + string encoded = pageUrl.Replace("mihoyobbs://webview?link=", string.Empty, StringComparison.OrdinalIgnoreCase); + targetUrl = Uri.UnescapeDataString(encoded); + } + } + + await taskContext.SwitchToMainThreadAsync(); + coreWebView2.Navigate(targetUrl); + return null; } public virtual IJsResult? Share(JsParam param) From 3057673cdbc22689c43ac4d69ebc0ae7e55c73e4 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 3 Nov 2023 10:11:47 +0800 Subject: [PATCH 04/24] fix #1069 --- .../View/Page/AvatarPropertyPage.xaml | 102 +++++++++++------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml index cadd6d8b..b1b53b4c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml @@ -253,6 +253,67 @@ + + + + @@ -659,44 +720,9 @@ Grid.Row="1" Grid.Column="0" Margin="16" + ItemTemplate="{StaticResource AvatarConstellationTemplate}" ItemsPanel="{StaticResource HorizontalStackPanelSpacing0Template}" - ItemsSource="{Binding SelectedAvatar.Constellations}"> - - - - - - + ItemsSource="{Binding SelectedAvatar.Constellations}"/> From 91de6d170ef814cea5e0c4af8f019687db1534f9 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 3 Nov 2023 11:52:52 +0800 Subject: [PATCH 05/24] add fingerprint fetch & fix #1060 --- ...31103032056_AddUserFingerprint.Designer.cs | 549 ++++++++++++++++++ .../20231103032056_AddUserFingerprint.cs | 29 + .../Migrations/AppDbContextModelSnapshot.cs | 7 +- .../Snap.Hutao/Model/Entity/User.cs | 5 + .../Service/User/IUserFingerprintService.cs | 9 + .../User/IUserInitializationService.cs | 2 +- .../Service/User/UserFingerprintService.cs | 113 ++++ .../Service/User/UserInitializationService.cs | 7 +- .../Snap.Hutao/Service/User/UserService.cs | 12 +- .../Snap.Hutao/ViewModel/User/User.cs | 4 + .../Web/Hoyolab/App/Account/AccountClient.cs | 2 +- .../Web/Hoyolab/Bbs/User/UserClientOversea.cs | 2 +- ...yolabHttpRequestMessageBuilderExtension.cs | 11 +- .../Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs | 1 - .../Web/Hoyolab/Passport/PassportClient.cs | 4 +- .../Web/Hoyolab/Passport/PassportClient2.cs | 2 +- .../Hoyolab/Passport/PassportClientOversea.cs | 4 +- .../PublicData/DeviceFp/DeviceFpClient.cs | 20 + .../PublicData/DeviceFp/DeviceFpData.cs | 2 +- .../PublicData/DeviceFp/DeviceFpWrapper.cs | 16 + .../Web/Hoyolab/Takumi/Auth/AuthClient.cs | 2 +- .../Hoyolab/Takumi/Binding/BindingClient.cs | 4 +- .../Hoyolab/Takumi/Binding/BindingClient2.cs | 4 +- .../Event/BbsSignReward/SignInClient.cs | 12 +- .../BbsSignReward/SignInClientOversea.cs | 8 +- .../Takumi/Event/Calculate/CalculateClient.cs | 10 +- .../Hoyolab/Takumi/GameRecord/CardClient.cs | 4 +- .../Takumi/GameRecord/GameRecordClient.cs | 18 +- .../GameRecord/GameRecordClientOversea.cs | 8 +- 29 files changed, 817 insertions(+), 54 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/User/IUserFingerprintService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpWrapper.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs new file mode 100644 index 00000000..2e90947e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs @@ -0,0 +1,549 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Model.Entity.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20231103032056_AddUserFingerprint")] + partial class AddUserFingerprint + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalculatorRefreshTime") + .HasColumnType("TEXT"); + + b.Property("GameRecordRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Info") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShowcaseRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("avatar_infos"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("cultivate_entries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachedUid") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("cultivate_projects"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DailyNote") + .HasColumnType("TEXT"); + + b.Property("DailyTaskNotify") + .HasColumnType("INTEGER"); + + b.Property("DailyTaskNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotify") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("ResinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ResinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotify") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("UserId"); + + b.ToTable("daily_notes"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("gacha_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachUid") + .HasColumnType("TEXT"); + + b.Property("MihoyoSDK") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("game_accounts"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppendPropIdList") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("MainPropId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_reliquaries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("PromoteLevel") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_weapons"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ExpireTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("object_cache"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SpiralAbyss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("spiral_abysses"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Aid") + .HasColumnType("TEXT"); + + b.Property("CookieToken") + .HasColumnType("TEXT"); + + b.Property("Fingerprint") + .HasColumnType("TEXT"); + + b.Property("IsOversea") + .HasColumnType("INTEGER"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LToken") + .HasColumnType("TEXT") + .HasColumnName("Ltoken"); + + b.Property("Mid") + .HasColumnType("TEXT"); + + b.Property("SToken") + .HasColumnType("TEXT") + .HasColumnName("Stoken"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.HasOne("Snap.Hutao.Model.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.cs new file mode 100644 index 00000000..772c35fe --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.cs @@ -0,0 +1,29 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + /// + public partial class AddUserFingerprint : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Fingerprint", + table: "users", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Fingerprint", + table: "users"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 646ae427..dd8d37f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => { @@ -400,7 +400,7 @@ namespace Snap.Hutao.Migrations .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("ScheduleId") + b.Property("ScheduleId") .HasColumnType("INTEGER"); b.Property("SpiralAbyss") @@ -428,6 +428,9 @@ namespace Snap.Hutao.Migrations b.Property("CookieToken") .HasColumnType("TEXT"); + b.Property("Fingerprint") + .HasColumnType("TEXT"); + b.Property("IsOversea") .HasColumnType("INTEGER"); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index 9a45b4ef..0b9f7bf0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -60,6 +60,11 @@ internal sealed class User : ISelectable, IMappingFrom /// public bool IsOversea { get; set; } + /// + /// 用户指纹 Id + /// + public string? Fingerprint { get; set; } + /// /// 创建一个新的用户 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserFingerprintService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserFingerprintService.cs new file mode 100644 index 00000000..9927dcbf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserFingerprintService.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.User; + +internal interface IUserFingerprintService +{ + ValueTask TryInitializeAsync(ViewModel.User.User user, CancellationToken token = default); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserInitializationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserInitializationService.cs index 18b906e4..03f62a30 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserInitializationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserInitializationService.cs @@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.User; internal interface IUserInitializationService { - ValueTask CreateOrDefaultUserFromCookieAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken)); + ValueTask CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken)); ValueTask ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default(CancellationToken)); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs new file mode 100644 index 00000000..bcb6ba86 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs @@ -0,0 +1,113 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.PublicData.DeviceFp; +using Snap.Hutao.Web.Response; +using System.Text; + +namespace Snap.Hutao.Service.User; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IUserFingerprintService))] +internal sealed partial class UserFingerprintService : IUserFingerprintService +{ + private readonly DeviceFpClient deviceFpClient; + + public async ValueTask TryInitializeAsync(ViewModel.User.User user, CancellationToken token = default) + { + if (user.IsOversea) + { + // disable HoYoLAB fp approach + return; + } + + if (!string.IsNullOrEmpty(user.Fingerprint)) + { + return; + } + + string model = GetRandomStringOfLength(6); + Dictionary extendProperties = new() + { + { "cpuType", "arm64-v8a" }, + { "romCapacity", "512" }, + { "productName", model }, + { "romRemain", "256" }, + { "manufacturer", "XiaoMi" }, + { "appMemory", "512" }, + { "hostname", "dg02-pool03-kvm87" }, + { "screenSize", "1080x1920" }, + { "osVersion", "13" }, + { "aaid", string.Empty }, + { "vendor", "中国移动" }, + { "accelerometer", "1.4883357x7.1712894x6.2847486" }, + { "buildTags", "release-keys" }, + { "model", model }, + { "brand", "XiaoMi" }, + { "oaid", string.Empty }, + { "hardware", "qcom" }, + { "deviceType", "OP5913L1" }, + { "devId", "REL" }, + { "serialNumber", "unknown" }, + { "buildTime", "1687848011000" }, + { "buildUser", "root" }, + { "ramCapacity", "469679" }, + { "magnetometer", "20.081251x-27.487501x2.1937501" }, + { "display", $"{model}_13.1.0.181(CN01)" }, + { "ramRemain", "215344" }, + { "deviceInfo", $@"XiaoMi/{model}/OP5913L1:13/SKQ1.221119.001/T.118e6c7-5aa23-73911:user/release-keys" }, + { "gyroscope", "0.030226856x0.014647375x0.010652636" }, + { "vaid", string.Empty }, + { "buildType", "user" }, + { "sdkVersion", "33" }, + { "board", "taro" }, + }; + + DeviceFpData data = new() + { + DeviceId = GetRandomHexStringOfLength(16), + SeedId = $"{Guid.NewGuid()}", + Platform = "2", + SeedTime = $"{DateTimeOffset.Now.ToUnixTimeMilliseconds()}", + ExtFields = JsonSerializer.Serialize(extendProperties), + AppName = "bbs_cn", + BbsDeviceId = HoyolabOptions.DeviceId, + DeviceFp = GetRandomHexStringOfLength(13), + }; + + Response response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false); + user.Fingerprint = response.IsOk() ? response.Data.DeviceFp : string.Empty; + user.NeedDbUpdateAfterResume = true; + } + + private static string GetRandomHexStringOfLength(int length) + { + const string RandomRange = "0123456789abcdef"; + + StringBuilder sb = new(length); + + for (int i = 0; i < length; i++) + { + int pos = Random.Shared.Next(0, RandomRange.Length); + sb.Append(RandomRange[pos]); + } + + return sb.ToString(); + } + + private static string GetRandomStringOfLength(int length) + { + const string RandomRange = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + StringBuilder sb = new(length); + + for (int i = 0; i < length; i++) + { + int pos = Random.Shared.Next(0, RandomRange.Length); + sb.Append(RandomRange[pos]); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs index baa26503..2d2dfb7c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs @@ -14,6 +14,7 @@ namespace Snap.Hutao.Service.User; [Injection(InjectAs.Singleton, typeof(IUserInitializationService))] internal sealed partial class UserInitializationService : IUserInitializationService { + private readonly IUserFingerprintService userFingerprintService; private readonly IServiceProvider serviceProvider; public async ValueTask ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default) @@ -29,7 +30,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer return user; } - public async ValueTask CreateOrDefaultUserFromCookieAsync(Cookie cookie, bool isOversea, CancellationToken token = default) + public async ValueTask CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default) { // 这里只负责创建实体用户,稍后在用户服务中保存到数据库 Model.Entity.User entity = Model.Entity.User.From(cookie, isOversea); @@ -64,8 +65,6 @@ internal sealed partial class UserInitializationService : IUserInitializationSer return false; } - bool isOversea = user.Entity.IsOversea; - if (!await TrySetUserLTokenAsync(user, token).ConfigureAwait(false)) { return false; @@ -86,6 +85,8 @@ internal sealed partial class UserInitializationService : IUserInitializationSer return false; } + await userFingerprintService.TryInitializeAsync(user, token).ConfigureAwait(false); + user.SelectedUserGameRole = user.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen); return user.IsInitialized = true; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index 9cf52879..6ec475cf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -73,6 +73,16 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe { List entities = await userDbService.GetUserListAsync().ConfigureAwait(false); List users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync, default).ConfigureAwait(false); + + foreach (BindingUser user in users) + { + if (user.NeedDbUpdateAfterResume) + { + await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false); + user.NeedDbUpdateAfterResume = false; + } + } + userCollection = users.ToObservableCollection(); try @@ -202,7 +212,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe private async ValueTask> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea) { await taskContext.SwitchToBackgroundAsync(); - BindingUser? newUser = await userInitializationService.CreateOrDefaultUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false); + BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false); if (newUser is not null) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/User.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/User.cs index 329b227a..d7cfc748 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/User.cs @@ -62,6 +62,8 @@ internal sealed class User : ObservableObject, IEntityOnly, IMapping } } + public string? Fingerprint { get => inner.Fingerprint; set => inner.Fingerprint = value; } + public Guid InnerId { get => inner.InnerId; } /// @@ -102,6 +104,8 @@ internal sealed class User : ObservableObject, IEntityOnly, IMapping /// public EntityUser Entity { get => inner; } + public bool NeedDbUpdateAfterResume { get; set; } + public static User From(EntityUser user, IServiceProvider provider) { return new(user, provider); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs index 9f97b281..538dc199 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs @@ -37,7 +37,7 @@ internal sealed partial class AccountClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.AppAuthGenAuthKey) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .SetReferer(ApiEndpoints.AppMihoyoReferer) .PostJson(data); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs index c5570b6c..ac719dfc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs @@ -34,7 +34,7 @@ internal sealed partial class UserClientOversea : IUserClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.UserFullInfoQuery(user.Aid)) - .SetUserCookie(user, CookieType.LToken) + .SetUserCookieAndFpHeader(user, CookieType.LToken) .Get(); Response? resp = await builder diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabHttpRequestMessageBuilderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabHttpRequestMessageBuilderExtension.cs index 3117d1c9..ef638446 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabHttpRequestMessageBuilderExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabHttpRequestMessageBuilderExtension.cs @@ -9,12 +9,12 @@ namespace Snap.Hutao.Web.Hoyolab; internal static class HoyolabHttpRequestMessageBuilderExtension { - internal static HttpRequestMessageBuilder SetUserCookie(this HttpRequestMessageBuilder builder, UserAndUid userAndUid, CookieType cookie) + internal static HttpRequestMessageBuilder SetUserCookieAndFpHeader(this HttpRequestMessageBuilder builder, UserAndUid userAndUid, CookieType cookie) { - return builder.SetUserCookie(userAndUid.User, cookie); + return builder.SetUserCookieAndFpHeader(userAndUid.User, cookie); } - internal static HttpRequestMessageBuilder SetUserCookie(this HttpRequestMessageBuilder builder, Model.Entity.User user, CookieType cookie) + internal static HttpRequestMessageBuilder SetUserCookieAndFpHeader(this HttpRequestMessageBuilder builder, Model.Entity.User user, CookieType cookie) { builder.RemoveHeader("Cookie"); StringBuilder stringBuilder = new(); @@ -40,6 +40,11 @@ internal static class HoyolabHttpRequestMessageBuilderExtension builder.AddHeader("Cookie", result); } + if (!string.IsNullOrEmpty(user.Fingerprint)) + { + builder.SetHeader("x-rpc-device_fp", user.Fingerprint); + } + return builder; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs index 984c105c..2ebb11ba 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs @@ -10,7 +10,6 @@ namespace Snap.Hutao.Web.Hoyolab; /// /// 米游社选项 /// -[Injection(InjectAs.Singleton)] internal sealed class HoyolabOptions : IOptions { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs index 6b8e951e..41c8cddf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs @@ -35,7 +35,7 @@ internal sealed partial class PassportClient : IPassportClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.AccountGetCookieTokenBySToken) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.PROD, true).ConfigureAwait(false); @@ -58,7 +58,7 @@ internal sealed partial class PassportClient : IPassportClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.AccountGetLTokenBySToken) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.PROD, true).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index 774cd82a..60bb4246 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -35,7 +35,7 @@ internal sealed partial class PassportClient2 { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.AccountVerifyLtoken) - .SetUserCookie(user, CookieType.LToken) + .SetUserCookieAndFpHeader(user, CookieType.LToken) .PostJson(new Timestamp()); Response? resp = await builder diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs index ec6be8fb..2f4a1ac8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs @@ -38,7 +38,7 @@ internal sealed partial class PassportClientOversea : IPassportClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.AccountGetCookieTokenBySToken) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .PostJson(data); Response? resp = await builder @@ -64,7 +64,7 @@ internal sealed partial class PassportClientOversea : IPassportClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.AccountGetLTokenBySToken) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .PostJson(data); Response? resp = await builder diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpClient.cs index d0d397c3..0b27db4a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpClient.cs @@ -2,11 +2,31 @@ // Licensed under the MIT license. using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Request.Builder; +using Snap.Hutao.Web.Request.Builder.Abstraction; +using Snap.Hutao.Web.Response; +using System.Net.Http; namespace Snap.Hutao.Web.Hoyolab.PublicData.DeviceFp; +[ConstructorGenerated(ResolveHttpClient = true)] [HttpClient(HttpClientConfiguration.Default)] internal sealed partial class DeviceFpClient { + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly HttpClient httpClient; + private readonly ILogger logger; + public async ValueTask> GetFingerprintAsync(DeviceFpData data, CancellationToken token) + { + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.DeviceFpGetFp) + .PostJson(data); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpData.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpData.cs index 7f744fb4..ae204176 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpData.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpData.cs @@ -27,5 +27,5 @@ internal sealed class DeviceFpData public string AppName { get; set; } = default!; [JsonPropertyName("ext_fields")] - public Dictionary ExtFields { get; set; } = default!; + public string ExtFields { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpWrapper.cs new file mode 100644 index 00000000..8b836ba7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PublicData/DeviceFp/DeviceFpWrapper.cs @@ -0,0 +1,16 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.PublicData.DeviceFp; + +internal sealed class DeviceFpWrapper +{ + [JsonPropertyName("device_fp")] + public string DeviceFp { get; set; } = default!; + + [JsonPropertyName("code")] + public int Code { get; set; } = default!; + + [JsonPropertyName("message")] + public string Message { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs index bb516638..bf941f7d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs @@ -33,7 +33,7 @@ internal sealed partial class AuthClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.AuthActionTicket(action, stoken, user.Aid)) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen1, SaltType.K2, true).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs index 8bde1ea1..bc057241 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs @@ -69,7 +69,7 @@ internal sealed partial class BindingClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.UserGameRolesByActionTicket(actionTicket)) - .SetUserCookie(user, CookieType.LToken) + .SetUserCookieAndFpHeader(user, CookieType.LToken) .Get(); Response>? resp = await builder @@ -90,7 +90,7 @@ internal sealed partial class BindingClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.UserGameRolesByCookie) - .SetUserCookie(user, CookieType.LToken) + .SetUserCookieAndFpHeader(user, CookieType.LToken) .Get(); Response>? resp = await builder diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs index e21013da..9d1f4e3a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs @@ -36,7 +36,7 @@ internal sealed partial class BindingClient2 { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.AccountGetCookieTokenBySToken) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .SetReferer(ApiEndpoints.AppMihoyoReferer) .Get(); @@ -62,7 +62,7 @@ internal sealed partial class BindingClient2 { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.BindingGenAuthKey) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .SetReferer(ApiEndpoints.AppMihoyoReferer) .PostJson(data); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClient.cs index 0d2ea50d..0ef4e4c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClient.cs @@ -29,7 +29,7 @@ internal sealed partial class SignInClient : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.SignInRewardInfo(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen1, SaltType.LK2, true).ConfigureAwait(false); @@ -45,7 +45,7 @@ internal sealed partial class SignInClient : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.SignInRewardResignInfo(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen1, SaltType.LK2, true).ConfigureAwait(false); @@ -61,7 +61,7 @@ internal sealed partial class SignInClient : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.SignInRewardHome) - .SetUserCookie(user, CookieType.CookieToken) + .SetUserCookieAndFpHeader(user, CookieType.CookieToken) .Get(); Response? resp = await builder @@ -75,7 +75,7 @@ internal sealed partial class SignInClient : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.SignInRewardReSign) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .PostJson(new SignInData(userAndUid.Uid, false)); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen1, SaltType.LK2, true).ConfigureAwait(false); @@ -91,7 +91,7 @@ internal sealed partial class SignInClient : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.SignInRewardSign) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .PostJson(new SignInData(userAndUid.Uid, false)); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen1, SaltType.LK2, true).ConfigureAwait(false); @@ -108,7 +108,7 @@ internal sealed partial class SignInClient : ISignInClient { HttpRequestMessageBuilder verifiedBuilder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.SignInRewardSign) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .SetXrpcChallenge(challenge, validate) .PostJson(new SignInData(userAndUid.Uid, false)); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClientOversea.cs index 9e5c30be..03a4ed74 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClientOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInClientOversea.cs @@ -28,7 +28,7 @@ internal sealed partial class SignInClientOversea : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.SignInRewardInfo(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .Get(); Response? resp = await builder @@ -42,7 +42,7 @@ internal sealed partial class SignInClientOversea : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.SignInRewardHome) - .SetUserCookie(user, CookieType.CookieToken) + .SetUserCookieAndFpHeader(user, CookieType.CookieToken) .Get(); Response? resp = await builder @@ -56,7 +56,7 @@ internal sealed partial class SignInClientOversea : ISignInClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.SignInRewardSign) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .PostJson(new SignInData(userAndUid.Uid, true)); Response? resp = await builder @@ -71,7 +71,7 @@ internal sealed partial class SignInClientOversea : ISignInClient { HttpRequestMessageBuilder verifiedBuilder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.SignInRewardSign) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .SetXrpcChallenge(challenge, validate) .PostJson(new SignInData(userAndUid.Uid, true)); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs index 42e0883b..79c6c126 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs @@ -35,7 +35,7 @@ internal sealed partial class CalculateClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(user.IsOversea ? ApiOsEndpoints.CalculateCompute : ApiEndpoints.CalculateCompute) - .SetUserCookie(user, CookieType.Cookie) + .SetUserCookieAndFpHeader(user, CookieType.Cookie) .SetReferer(user.IsOversea ? ApiOsEndpoints.ActHoyolabReferer : ApiEndpoints.WebStaticMihoyoReferer) .PostJson(delta); @@ -66,7 +66,7 @@ internal sealed partial class CalculateClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(userAndUid.User.IsOversea ? ApiOsEndpoints.CalculateSyncAvatarList : ApiEndpoints.CalculateSyncAvatarList) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .SetReferer(userAndUid.User.IsOversea ? ApiOsEndpoints.ActHoyolabReferer : ApiEndpoints.WebStaticMihoyoReferer) .PostJson(filter); @@ -106,7 +106,7 @@ internal sealed partial class CalculateClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(url) - .SetUserCookie(userAndUid, CookieType.CookieToken) + .SetUserCookieAndFpHeader(userAndUid, CookieType.CookieToken) .SetReferer(userAndUid.User.IsOversea ? ApiOsEndpoints.ActHoyolabReferer : ApiEndpoints.WebStaticMihoyoReferer) .Get(); @@ -128,7 +128,7 @@ internal sealed partial class CalculateClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.CalculateFurnitureBlueprint(shareCode)) - .SetUserCookie(user, CookieType.CookieToken) + .SetUserCookieAndFpHeader(user, CookieType.CookieToken) .SetReferer(user.IsOversea ? ApiOsEndpoints.ActHoyolabReferer : ApiEndpoints.WebStaticMihoyoReferer) .Get(); @@ -152,7 +152,7 @@ internal sealed partial class CalculateClient HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.CalculateFurnitureCompute) - .SetUserCookie(user, CookieType.CookieToken) + .SetUserCookieAndFpHeader(user, CookieType.CookieToken) .SetReferer(user.IsOversea ? ApiOsEndpoints.ActHoyolabReferer : ApiEndpoints.WebStaticMihoyoReferer) .PostJson(data); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs index 6f60cbce..28eb3b4e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs @@ -35,7 +35,7 @@ internal sealed partial class CardClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.CardCreateVerification(false)) - .SetUserCookie(user, CookieType.LToken) + .SetUserCookieAndFpHeader(user, CookieType.LToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X4, false).ConfigureAwait(false); @@ -80,7 +80,7 @@ internal sealed partial class CardClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.CardWidgetData2) - .SetUserCookie(user, CookieType.SToken) + .SetUserCookieAndFpHeader(user, CookieType.SToken) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X6, false).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs index b0a99155..a12c3088 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs @@ -39,7 +39,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X4, false).ConfigureAwait(false); @@ -59,7 +59,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .SetXrpcChallenge(challenge) .Get(); @@ -85,7 +85,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X4, false).ConfigureAwait(false); @@ -105,7 +105,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .SetXrpcChallenge(challenge) .Get(); @@ -132,7 +132,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X4, false).ConfigureAwait(false); @@ -152,7 +152,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .SetXrpcChallenge(challenge) .Get(); @@ -178,7 +178,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordRoleBasicInfo(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X4, false).ConfigureAwait(false); @@ -202,7 +202,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordCharacter) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id))); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.X4, false).ConfigureAwait(false); @@ -222,7 +222,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient { HttpRequestMessageBuilder verifiedBuilder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiEndpoints.GameRecordCharacter) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .SetXrpcChallenge(challenge) .PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id))); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs index ea3d6c9d..7c5005ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs @@ -36,7 +36,7 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.OSX4, false).ConfigureAwait(false); @@ -59,7 +59,7 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.OSX4, false).ConfigureAwait(false); @@ -83,7 +83,7 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid)) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .Get(); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.OSX4, false).ConfigureAwait(false); @@ -107,7 +107,7 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient { HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() .SetRequestUri(ApiOsEndpoints.GameRecordCharacter) - .SetUserCookie(userAndUid, CookieType.Cookie) + .SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie) .PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id))); await builder.SetDynamicSecretAsync(DynamicSecretVersion.Gen2, SaltType.OSX4, false).ConfigureAwait(false); From 87e5ede91fc944ce80ef8295f8d343cf8d225574 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 3 Nov 2023 16:20:11 +0800 Subject: [PATCH 06/24] impl #1068 --- .../Layout/UniformStaggeredColumnLayout.cs | 24 ++ .../Control/Layout/UniformStaggeredItem.cs | 29 ++ .../Control/Layout/UniformStaggeredLayout.cs | 270 ++++++++++++++++++ .../Layout/UniformStaggeredLayoutState.cs | 188 ++++++++++++ .../Snap.Hutao/View/Page/CultivationPage.xaml | 15 +- 5 files changed, 520 insertions(+), 6 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredColumnLayout.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredItem.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredColumnLayout.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredColumnLayout.cs new file mode 100644 index 00000000..b7aa699a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredColumnLayout.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; + +namespace Snap.Hutao.Control.Layout; + +[DebuggerDisplay("Count = {Count}, Height = {Height}")] +internal class UniformStaggeredColumnLayout : List +{ + public double Height { get; private set; } + + public new void Add(UniformStaggeredItem item) + { + Height = item.Top + item.Height; + base.Add(item); + } + + public new void Clear() + { + Height = 0; + base.Clear(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredItem.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredItem.cs new file mode 100644 index 00000000..3ae32b3c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredItem.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snap.Hutao.Control.Layout; + +internal sealed class UniformStaggeredItem +{ + public UniformStaggeredItem(int index) + { + Index = index; + } + + public double Top { get; internal set; } + + public double Height { get; internal set; } + + public int Index { get; } + + public UIElement? Element { get; internal set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs new file mode 100644 index 00000000..e2ce7a17 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs @@ -0,0 +1,270 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Collections.Specialized; +using Windows.Foundation; + +namespace Snap.Hutao.Control.Layout; + +[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))] +[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))] +[DependencyProperty("MinRowSpacing", typeof(double), 0D, nameof(OnSpacingChanged))] +internal sealed partial class UniformStaggeredLayout : VirtualizingLayout +{ + /// + protected override void InitializeForContextCore(VirtualizingLayoutContext context) + { + context.LayoutState = new UniformStaggeredLayoutState(context); + base.InitializeForContextCore(context); + } + + /// + protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + context.LayoutState = null; + base.UninitializeForContextCore(context); + } + + /// + protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args) + { + UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState; + + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + state.RemoveFromIndex(args.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Replace: + state.RemoveFromIndex(args.NewStartingIndex); + + // We must recycle the element to ensure that it gets the correct context + state.RecycleElementAt(args.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Move: + int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); + int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex); + state.RemoveRange(minIndex, maxIndex); + break; + case NotifyCollectionChangedAction.Remove: + state.RemoveFromIndex(args.OldStartingIndex); + break; + case NotifyCollectionChangedAction.Reset: + state.Clear(); + break; + } + + base.OnItemsChangedCore(context, source, args); + } + + /// + protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + if (context.ItemCount == 0) + { + return new Size(availableSize.Width, 0); + } + + if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0)) + { + return new Size(availableSize.Width, 0.0f); + } + + UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState; + + double availableWidth = availableSize.Width; + double availableHeight = availableSize.Height; + + (int columnCount, double columnWidth) = GetColumnInfo(availableWidth, MinItemWidth, MinColumnSpacing); + + if (columnWidth != state.ColumnWidth) + { + // The items will need to be remeasured + state.Clear(); + } + + state.ColumnWidth = columnWidth; + + // adjust for column spacing on all columns expect the first + double totalWidth = state.ColumnWidth + ((columnCount - 1) * (state.ColumnWidth + MinColumnSpacing)); + if (totalWidth > availableWidth) + { + columnCount--; + } + else if (double.IsInfinity(availableWidth)) + { + availableWidth = totalWidth; + } + + if (columnCount != state.NumberOfColumns) + { + // The items will not need to be remeasured, but they will need to go into new columns + state.ClearColumns(); + } + + if (MinRowSpacing != state.RowSpacing) + { + // If the RowSpacing changes the height of the rows will be different. + // The columns stores the height so we'll want to clear them out to + // get the proper height + state.ClearColumns(); + state.RowSpacing = MinRowSpacing; + } + + double[] columnHeights = new double[columnCount]; + int[] itemsPerColumn = new int[columnCount]; + HashSet deadColumns = new(); + + for (int i = 0; i < context.ItemCount; i++) + { + int columnIndex = GetColumnIndex(columnHeights); + + bool measured = false; + UniformStaggeredItem item = state.GetItemAt(i); + if (item.Height == 0) + { + // Item has not been measured yet. Get the element and store the values + item.Element = context.GetOrCreateElementAt(i); + item.Element.Measure(new Size((float)state.ColumnWidth, (float)availableHeight)); + item.Height = item.Element.DesiredSize.Height; + measured = true; + } + + double spacing = itemsPerColumn[columnIndex] > 0 ? MinRowSpacing : 0; + item.Top = columnHeights[columnIndex] + spacing; + double bottom = item.Top + item.Height; + columnHeights[columnIndex] = bottom; + itemsPerColumn[columnIndex]++; + state.AddItemToColumn(item, columnIndex); + + if (bottom < context.RealizationRect.Top) + { + // The bottom of the element is above the realization area + if (item.Element is not null) + { + context.RecycleElement(item.Element); + item.Element = null; + } + } + else if (item.Top > context.RealizationRect.Bottom) + { + // The top of the element is below the realization area + if (item.Element is not null) + { + context.RecycleElement(item.Element); + item.Element = null; + } + + deadColumns.Add(columnIndex); + } + else if (measured == false) + { + // We ALWAYS want to measure an item that will be in the bounds + item.Element = context.GetOrCreateElementAt(i); + item.Element.Measure(new Size((float)state.ColumnWidth, (float)availableHeight)); + if (item.Height != item.Element.DesiredSize.Height) + { + // this item changed size; we need to recalculate layout for everything after this + state.RemoveFromIndex(i + 1); + item.Height = item.Element.DesiredSize.Height; + columnHeights[columnIndex] = item.Top + item.Height; + } + } + + if (deadColumns.Count == columnCount) + { + break; + } + } + + double desiredHeight = state.GetHeight(); + + return new Size((float)availableWidth, (float)desiredHeight); + } + + /// + protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0)) + { + return finalSize; + } + + UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState; + + // Cycle through each column and arrange the items that are within the realization bounds + for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++) + { + UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex); + for (int i = 0; i < layout.Count; i++) + { + UniformStaggeredItem item = layout[i]; + + double bottom = item.Top + item.Height; + if (bottom < context.RealizationRect.Top) + { + // element is above the realization bounds + continue; + } + + if (item.Top <= context.RealizationRect.Bottom) + { + double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex); + + Rect bounds = new((float)itemHorizontalOffset, (float)item.Top, (float)state.ColumnWidth, (float)item.Height); + UIElement element = context.GetOrCreateElementAt(item.Index); + element.Arrange(bounds); + } + else + { + break; + } + } + } + + return finalSize; + } + + private static (int ColumnCount, double ColumnWidth) GetColumnInfo(double availableWidth, double minItemWidth, double minColumnSpacing) + { + // less than 2 item per row + if ((2 * minItemWidth) + minColumnSpacing > availableWidth) + { + return (1, availableWidth); + } + + int columnCount = (int)Math.Max(1, Math.Floor((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing))); + double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount; + return (columnCount, columnWidthAddSpacing - minColumnSpacing); + } + + private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UniformStaggeredLayout panel = (UniformStaggeredLayout)d; + panel.InvalidateMeasure(); + } + + private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UniformStaggeredLayout panel = (UniformStaggeredLayout)d; + panel.InvalidateMeasure(); + } + + private static int GetColumnIndex(double[] columnHeights) + { + int columnIndex = 0; + double height = columnHeights[0]; + for (int j = 1; j < columnHeights.Length; j++) + { + if (columnHeights[j] < height) + { + columnIndex = j; + height = columnHeights[j]; + } + } + + return columnIndex; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs new file mode 100644 index 00000000..20b3b52f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs @@ -0,0 +1,188 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Snap.Hutao.Control.Layout; + +internal sealed class UniformStaggeredLayoutState +{ + private readonly List items = new(); + private readonly VirtualizingLayoutContext context; + private readonly Dictionary columnLayout = new(); + private double lastAverageHeight; + + public UniformStaggeredLayoutState(VirtualizingLayoutContext context) + { + this.context = context; + } + + public double ColumnWidth { get; internal set; } + + public int NumberOfColumns + { + get => columnLayout.Count; + } + + public double RowSpacing { get; internal set; } + + internal void AddItemToColumn(UniformStaggeredItem item, int columnIndex) + { + if (this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout) == false) + { + columnLayout = new UniformStaggeredColumnLayout(); + this.columnLayout[columnIndex] = columnLayout; + } + + if (columnLayout.Contains(item) == false) + { + columnLayout.Add(item); + } + } + + internal UniformStaggeredItem GetItemAt(int index) + { + if (index < 0) + { + throw new IndexOutOfRangeException(); + } + + if (index <= (items.Count - 1)) + { + return items[index]; + } + else + { + UniformStaggeredItem item = new(index); + items.Add(item); + return item; + } + } + + internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex) + { + this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout); + return columnLayout!; + } + + /// + /// Clear everything that has been calculated. + /// + internal void Clear() + { + columnLayout.Clear(); + items.Clear(); + } + + /// + /// Clear the layout columns so they will be recalculated. + /// + internal void ClearColumns() + { + columnLayout.Clear(); + } + + /// + /// Gets the estimated height of the layout. + /// + /// The estimated height of the layout. + /// + /// If all of the items have been calculated then the actual height will be returned. + /// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items. + /// + internal double GetHeight() + { + double desiredHeight = Enumerable.Max(columnLayout.Values, c => c.Height); + + int itemCount = Enumerable.Sum(columnLayout.Values, c => c.Count); + if (itemCount == context.ItemCount) + { + return desiredHeight; + } + + double averageHeight = 0; + foreach (KeyValuePair kvp in columnLayout) + { + averageHeight += kvp.Value.Height / kvp.Value.Count; + } + + averageHeight /= columnLayout.Count; + double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count; + if (estimatedHeight > desiredHeight) + { + desiredHeight = estimatedHeight; + } + + if (Math.Abs(desiredHeight - lastAverageHeight) < 5) + { + return lastAverageHeight; + } + + lastAverageHeight = desiredHeight; + return desiredHeight; + } + + internal void RecycleElementAt(int index) + { + UIElement element = context.GetOrCreateElementAt(index); + context.RecycleElement(element); + } + + internal void RemoveFromIndex(int index) + { + if (index >= items.Count) + { + // Item was added/removed but we haven't realized that far yet + return; + } + + int numToRemove = items.Count - index; + items.RemoveRange(index, numToRemove); + + foreach (KeyValuePair kvp in columnLayout) + { + UniformStaggeredColumnLayout layout = kvp.Value; + for (int i = 0; i < layout.Count; i++) + { + if (layout[i].Index >= index) + { + numToRemove = layout.Count - i; + layout.RemoveRange(i, numToRemove); + break; + } + } + } + } + + internal void RemoveRange(int startIndex, int endIndex) + { + for (int i = startIndex; i <= endIndex; i++) + { + if (i > items.Count) + { + break; + } + + UniformStaggeredItem item = items[i]; + item.Height = 0; + item.Top = 0; + + // We must recycle all elements to ensure that it gets the correct context + RecycleElementAt(i); + } + + foreach ((int key, UniformStaggeredColumnLayout layout) in columnLayout) + { + for (int i = 0; i < layout.Count; i++) + { + if ((startIndex <= layout[i].Index) && (layout[i].Index <= endIndex)) + { + int numToRemove = layout.Count - i; + layout.RemoveRange(i, numToRemove); + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml index 33d02646..1510dc9e 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml @@ -12,6 +12,7 @@ xmlns:shc="using:Snap.Hutao.Control" xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shci="using:Snap.Hutao.Control.Image" + xmlns:shcl="using:Snap.Hutao.Control.Layout" xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shvco="using:Snap.Hutao.View.Control" xmlns:shvcu="using:Snap.Hutao.ViewModel.Cultivation" @@ -149,10 +150,7 @@ - + - + From aeb6962ae4992958b7c7ab96fbedea5299333186 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Fri, 3 Nov 2023 22:06:51 +0800 Subject: [PATCH 07/24] impl #1015 --- src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml | 7 ++++++- .../Web/Hutao/HutaoAsAService/UploadAnnouncement.cs | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml index 8e8541f1..29bb3eec 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml @@ -104,7 +104,8 @@ + Header="Upload Announcement" + IsExpanded="True"> + public string Link { get; set; } = default!; + + /// + /// 最高呈现版本 + /// + public string? MaxPresentVersion { get; set; } } \ No newline at end of file From 24086ee4d00cbecd4843ebf2f5ba2200cadb1a05 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Fri, 3 Nov 2023 23:32:52 +0800 Subject: [PATCH 08/24] optimize UniformStaggeredColumnLayout --- .../Control/Layout/UniformStaggeredLayout.cs | 75 ++++++++++--------- .../Layout/UniformStaggeredLayoutState.cs | 39 +++++----- 2 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs index e2ce7a17..43d82481 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs @@ -4,6 +4,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System.Collections.Specialized; +using System.Runtime.InteropServices; using Windows.Foundation; namespace Snap.Hutao.Control.Layout; @@ -37,20 +38,22 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout case NotifyCollectionChangedAction.Add: state.RemoveFromIndex(args.NewStartingIndex); break; + case NotifyCollectionChangedAction.Replace: state.RemoveFromIndex(args.NewStartingIndex); - - // We must recycle the element to ensure that it gets the correct context - state.RecycleElementAt(args.NewStartingIndex); + state.RecycleElementAt(args.NewStartingIndex); // We must recycle the element to ensure that it gets the correct context break; + case NotifyCollectionChangedAction.Move: int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex); state.RemoveRange(minIndex, maxIndex); break; + case NotifyCollectionChangedAction.Remove: state.RemoveFromIndex(args.OldStartingIndex); break; + case NotifyCollectionChangedAction.Reset: state.Clear(); break; @@ -77,7 +80,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout double availableWidth = availableSize.Width; double availableHeight = availableSize.Height; - (int columnCount, double columnWidth) = GetColumnInfo(availableWidth, MinItemWidth, MinColumnSpacing); + (int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing); if (columnWidth != state.ColumnWidth) { @@ -88,17 +91,17 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout state.ColumnWidth = columnWidth; // adjust for column spacing on all columns expect the first - double totalWidth = state.ColumnWidth + ((columnCount - 1) * (state.ColumnWidth + MinColumnSpacing)); + double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing)); if (totalWidth > availableWidth) { - columnCount--; + numberOfColumns--; } else if (double.IsInfinity(availableWidth)) { availableWidth = totalWidth; } - if (columnCount != state.NumberOfColumns) + if (numberOfColumns != state.NumberOfColumns) { // The items will not need to be remeasured, but they will need to go into new columns state.ClearColumns(); @@ -113,22 +116,23 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout state.RowSpacing = MinRowSpacing; } - double[] columnHeights = new double[columnCount]; - int[] itemsPerColumn = new int[columnCount]; + Span columnHeights = new double[numberOfColumns]; + Span itemsPerColumn = new int[numberOfColumns]; HashSet deadColumns = new(); for (int i = 0; i < context.ItemCount; i++) { - int columnIndex = GetColumnIndex(columnHeights); + int columnIndex = GetLowestColumnIndex(columnHeights); bool measured = false; UniformStaggeredItem item = state.GetItemAt(i); if (item.Height == 0) { // Item has not been measured yet. Get the element and store the values - item.Element = context.GetOrCreateElementAt(i); - item.Element.Measure(new Size((float)state.ColumnWidth, (float)availableHeight)); - item.Height = item.Element.DesiredSize.Height; + UIElement element = context.GetOrCreateElementAt(i); + element.Measure(new Size(state.ColumnWidth, availableHeight)); + item.Height = element.DesiredSize.Height; + item.Element = element; measured = true; } @@ -163,7 +167,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout { // We ALWAYS want to measure an item that will be in the bounds item.Element = context.GetOrCreateElementAt(i); - item.Element.Measure(new Size((float)state.ColumnWidth, (float)availableHeight)); + item.Element.Measure(new Size(state.ColumnWidth, availableHeight)); if (item.Height != item.Element.DesiredSize.Height) { // this item changed size; we need to recalculate layout for everything after this @@ -173,7 +177,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout } } - if (deadColumns.Count == columnCount) + if (deadColumns.Count == numberOfColumns) { break; } @@ -181,7 +185,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout double desiredHeight = state.GetHeight(); - return new Size((float)availableWidth, (float)desiredHeight); + return new Size(availableWidth, desiredHeight); } /// @@ -198,9 +202,10 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++) { UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex); - for (int i = 0; i < layout.Count; i++) + Span layoutSpan = CollectionsMarshal.AsSpan(layout); + for (int i = 0; i < layoutSpan.Length; i++) { - UniformStaggeredItem item = layout[i]; + ref readonly UniformStaggeredItem item = ref layoutSpan[i]; double bottom = item.Top + item.Height; if (bottom < context.RealizationRect.Top) @@ -213,7 +218,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout { double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex); - Rect bounds = new((float)itemHorizontalOffset, (float)item.Top, (float)state.ColumnWidth, (float)item.Height); + Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height); UIElement element = context.GetOrCreateElementAt(item.Index); element.Arrange(bounds); } @@ -227,32 +232,20 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout return finalSize; } - private static (int ColumnCount, double ColumnWidth) GetColumnInfo(double availableWidth, double minItemWidth, double minColumnSpacing) + private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing) { - // less than 2 item per row + // test if the width can fit in 2 items if ((2 * minItemWidth) + minColumnSpacing > availableWidth) { return (1, availableWidth); } - int columnCount = (int)Math.Max(1, Math.Floor((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing))); + int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing))); double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount; return (columnCount, columnWidthAddSpacing - minColumnSpacing); } - private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - UniformStaggeredLayout panel = (UniformStaggeredLayout)d; - panel.InvalidateMeasure(); - } - - private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - UniformStaggeredLayout panel = (UniformStaggeredLayout)d; - panel.InvalidateMeasure(); - } - - private static int GetColumnIndex(double[] columnHeights) + private static int GetLowestColumnIndex(in ReadOnlySpan columnHeights) { int columnIndex = 0; double height = columnHeights[0]; @@ -267,4 +260,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout return columnIndex; } + + private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UniformStaggeredLayout panel = (UniformStaggeredLayout)d; + panel.InvalidateMeasure(); + } + + private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UniformStaggeredLayout panel = (UniformStaggeredLayout)d; + panel.InvalidateMeasure(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs index 20b3b52f..70515422 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System.Runtime.InteropServices; namespace Snap.Hutao.Control.Layout; @@ -29,18 +30,19 @@ internal sealed class UniformStaggeredLayoutState internal void AddItemToColumn(UniformStaggeredItem item, int columnIndex) { - if (this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout) == false) + if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout)) { - columnLayout = new UniformStaggeredColumnLayout(); + columnLayout = new(); this.columnLayout[columnIndex] = columnLayout; } - if (columnLayout.Contains(item) == false) + if (!columnLayout.Contains(item)) { columnLayout.Add(item); } } + [SuppressMessage("", "CA2201")] internal UniformStaggeredItem GetItemAt(int index) { if (index < 0) @@ -93,18 +95,18 @@ internal sealed class UniformStaggeredLayoutState /// internal double GetHeight() { - double desiredHeight = Enumerable.Max(columnLayout.Values, c => c.Height); + double desiredHeight = columnLayout.Values.Max(c => c.Height); + int itemCount = columnLayout.Values.Sum(c => c.Count); - int itemCount = Enumerable.Sum(columnLayout.Values, c => c.Count); if (itemCount == context.ItemCount) { return desiredHeight; } double averageHeight = 0; - foreach (KeyValuePair kvp in columnLayout) + foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout) { - averageHeight += kvp.Value.Height / kvp.Value.Count; + averageHeight += layout.Height / layout.Count; } averageHeight /= columnLayout.Count; @@ -114,7 +116,7 @@ internal sealed class UniformStaggeredLayoutState desiredHeight = estimatedHeight; } - if (Math.Abs(desiredHeight - lastAverageHeight) < 5) + if (Math.Abs(desiredHeight - lastAverageHeight) < 5) // Why 5? { return lastAverageHeight; } @@ -140,14 +142,14 @@ internal sealed class UniformStaggeredLayoutState int numToRemove = items.Count - index; items.RemoveRange(index, numToRemove); - foreach (KeyValuePair kvp in columnLayout) + foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout) { - UniformStaggeredColumnLayout layout = kvp.Value; - for (int i = 0; i < layout.Count; i++) + Span layoutSpan = CollectionsMarshal.AsSpan(layout); + for (int i = 0; i < layoutSpan.Length; i++) { - if (layout[i].Index >= index) + if (layoutSpan[i].Index >= index) { - numToRemove = layout.Count - i; + numToRemove = layoutSpan.Length - i; layout.RemoveRange(i, numToRemove); break; } @@ -164,7 +166,7 @@ internal sealed class UniformStaggeredLayoutState break; } - UniformStaggeredItem item = items[i]; + ref readonly UniformStaggeredItem item = ref CollectionsMarshal.AsSpan(items)[i]; item.Height = 0; item.Top = 0; @@ -172,13 +174,14 @@ internal sealed class UniformStaggeredLayoutState RecycleElementAt(i); } - foreach ((int key, UniformStaggeredColumnLayout layout) in columnLayout) + foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout) { - for (int i = 0; i < layout.Count; i++) + Span layoutSpan = CollectionsMarshal.AsSpan(layout); + for (int i = 0; i < layoutSpan.Length; i++) { - if ((startIndex <= layout[i].Index) && (layout[i].Index <= endIndex)) + if ((startIndex <= layoutSpan[i].Index) && (layoutSpan[i].Index <= endIndex)) { - int numToRemove = layout.Count - i; + int numToRemove = layoutSpan.Length - i; layout.RemoveRange(i, numToRemove); break; } From 749ef0e138d9312f4e2242e0356a174396f1fc8c Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Sat, 4 Nov 2023 16:53:08 +0800 Subject: [PATCH 09/24] introducing game service facade --- .../Layout/UniformStaggeredLayoutState.cs | 3 +- .../Snap.Hutao/Control/ValueConverter.cs | 15 - .../DailyNoteNotificationOperation.cs | 2 +- .../GachaLogQueryWebCacheProvider.cs | 2 +- .../Game/Account/GameAccountService.cs | 135 ++++++ .../Game/Account/IGameAccountService.cs | 24 ++ .../Game/{ => Account}/RegistryInterop.cs | 2 +- .../{ => Configuration}/ChannelOptions.cs | 2 +- .../GameChannelOptionsService.cs | 95 +++++ .../IGameChannelOptionsService.cs | 13 + .../IgnoredInvalidChannelOptions.cs | 2 +- .../Service/Game/GamePathService.cs | 60 +++ .../Service/Game/GameProcessService.cs | 128 ++++++ .../Snap.Hutao/Service/Game/GameService.cs | 395 ------------------ .../Service/Game/GameServiceFacade.cs | 104 +++++ .../Service/Game/IGamePathService.cs | 9 + .../Service/Game/IGameProcessService.cs | 11 + ...{IGameService.cs => IGameServiceFacade.cs} | 5 +- .../Snap.Hutao/Service/Game/LaunchStatus.cs | 16 +- .../Game/Package/GamePackageService.cs | 67 +++ .../Game/Package/IGamePackageService.cs | 11 + .../Service/Game/Package/PackageConverter.cs | 1 + .../Snap.Hutao/Service/Game/ProcessInterop.cs | 162 ------- .../Game/{ => Scheme}/KnownLaunchSchemes.cs | 2 +- .../Service/Game/{ => Scheme}/LaunchScheme.cs | 5 +- .../Game/{ => Scheme}/LaunchSchemeBilibili.cs | 2 +- .../Game/{ => Scheme}/LaunchSchemeChinese.cs | 2 +- .../Game/{ => Scheme}/LaunchSchemeOversea.cs | 2 +- .../Snap.Hutao/View/Page/GachaLogPage.xaml | 10 +- .../ViewModel/Game/LaunchGameViewModel.cs | 4 +- .../ViewModel/Game/LaunchGameViewModelSlim.cs | 2 +- src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 2 +- .../Snap.Hutao/Web/ApiOsEndpoints.cs | 2 +- .../SdkStatic/Hk4e/Launcher/ResourceClient.cs | 2 +- 34 files changed, 703 insertions(+), 596 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Account}/RegistryInterop.cs (98%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Configuration}/ChannelOptions.cs (97%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Configuration}/IgnoredInvalidChannelOptions.cs (91%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/{IGameService.cs => IGameServiceFacade.cs} (94%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Scheme}/KnownLaunchSchemes.cs (98%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Scheme}/LaunchScheme.cs (94%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Scheme}/LaunchSchemeBilibili.cs (93%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Scheme}/LaunchSchemeChinese.cs (93%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => Scheme}/LaunchSchemeOversea.cs (93%) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs index 70515422..bb44b651 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs @@ -62,6 +62,7 @@ internal sealed class UniformStaggeredLayoutState } } + [SuppressMessage("", "SH007")] internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex) { this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout); @@ -116,7 +117,7 @@ internal sealed class UniformStaggeredLayoutState desiredHeight = estimatedHeight; } - if (Math.Abs(desiredHeight - lastAverageHeight) < 5) // Why 5? + if (Math.Abs(desiredHeight - lastAverageHeight) < 5) { return lastAverageHeight; } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs b/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs index c8b322cf..facb12b7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/ValueConverter.cs @@ -15,22 +15,7 @@ internal abstract class ValueConverter : IValueConverter /// public object? Convert(object value, Type targetType, object parameter, string language) { -#if DEBUG - try - { - return Convert((TFrom)value); - } - catch (Exception ex) - { - Ioc.Default - .GetRequiredService>>() - .LogError(ex, "值转换器异常"); - - throw; - } -#else return Convert((TFrom)value); -#endif } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs index 2286856b..1c5890ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs @@ -24,7 +24,7 @@ internal sealed partial class DailyNoteNotificationOperation private const string ToastAttributionUnknown = "Unknown"; private readonly ITaskContext taskContext; - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly BindingClient bindingClient; private readonly DailyNoteOptions options; 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 45d02c9c..635c336c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryWebCacheProvider.cs @@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider; [Injection(InjectAs.Transient)] internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider { - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly MetadataOptions metadataOptions; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs new file mode 100644 index 00000000..ff717fab --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs @@ -0,0 +1,135 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.View.Dialog; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Game.Account; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameAccountService))] +internal sealed partial class GameAccountService : IGameAccountService +{ + private readonly IContentDialogFactory contentDialogFactory; + private readonly IServiceProvider serviceProvider; + private readonly IGameDbService gameDbService; + private readonly ITaskContext taskContext; + private readonly AppOptions appOptions; + + private ObservableCollection? gameAccounts; + + public ObservableCollection GameAccountCollection + { + get => gameAccounts ??= gameDbService.GetGameAccountCollection(); + } + + public async ValueTask DetectGameAccountAsync() + { + ArgumentNullException.ThrowIfNull(gameAccounts); + + string? registrySdk = RegistryInterop.Get(); + if (!string.IsNullOrEmpty(registrySdk)) + { + GameAccount? account = null; + try + { + account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); + } + catch (InvalidOperationException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); + } + + if (account is null) + { + // ContentDialog must be created by main thread. + await taskContext.SwitchToMainThreadAsync(); + LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); + + if (isOk) + { + account = GameAccount.From(name, registrySdk); + + // sync database + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); + + // sync cache + await taskContext.SwitchToMainThreadAsync(); + gameAccounts.Add(account); + } + } + + return account; + } + + return default; + } + + public GameAccount? DetectCurrentGameAccount() + { + ArgumentNullException.ThrowIfNull(gameAccounts); + + string? registrySdk = RegistryInterop.Get(); + + if (!string.IsNullOrEmpty(registrySdk)) + { + try + { + return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); + } + catch (InvalidOperationException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); + } + } + + return null; + } + + public bool SetGameAccount(GameAccount account) + { + if (string.IsNullOrEmpty(appOptions.PowerShellPath)) + { + ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!); + } + + return RegistryInterop.Set(account, appOptions.PowerShellPath); + } + + public void AttachGameAccountToUid(GameAccount gameAccount, string uid) + { + gameAccount.UpdateAttachUid(uid); + gameDbService.UpdateGameAccount(gameAccount); + } + + public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) + { + await taskContext.SwitchToMainThreadAsync(); + LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance(); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true); + + if (isOk) + { + gameAccount.UpdateName(name); + + // sync database + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false); + } + } + + public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) + { + await taskContext.SwitchToMainThreadAsync(); + ArgumentNullException.ThrowIfNull(gameAccounts); + gameAccounts.Remove(gameAccount); + + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs new file mode 100644 index 00000000..108a35a6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Game.Account; + +internal interface IGameAccountService +{ + ObservableCollection GameAccountCollection { get; } + + void AttachGameAccountToUid(GameAccount gameAccount, string uid); + + GameAccount? DetectCurrentGameAccount(); + + ValueTask DetectGameAccountAsync(); + + ValueTask ModifyGameAccountAsync(GameAccount gameAccount); + + ValueTask RemoveGameAccountAsync(GameAccount gameAccount); + + bool SetGameAccount(GameAccount account); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs similarity index 98% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs index 97bbd4d5..29e8d129 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/RegistryInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs @@ -9,7 +9,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Account; /// /// 注册表操作 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ChannelOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs similarity index 97% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/ChannelOptions.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs index 8ebb4027..5c335637 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ChannelOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Configuration; /// /// 多通道 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs new file mode 100644 index 00000000..20db8658 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs @@ -0,0 +1,95 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Core.IO.Ini; +using Snap.Hutao.Service.Game.Scheme; +using System.IO; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game.Configuration; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))] +internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService +{ + private readonly AppOptions appOptions; + + public ChannelOptions GetChannelOptions() + { + string gamePath = appOptions.GamePath; + string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); + bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); + + if (!File.Exists(configPath)) + { + return ChannelOptions.FileNotFound(isOversea, configPath); + } + + using (FileStream stream = File.OpenRead(configPath)) + { + List parameters = IniSerializer.Deserialize(stream).OfType().ToList(); + string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value; + string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value; + + return new(channel, subChannel, isOversea); + } + } + + public bool SetChannelOptions(LaunchScheme scheme) + { + string gamePath = appOptions.GamePath; + string? directory = Path.GetDirectoryName(gamePath); + ArgumentException.ThrowIfNullOrEmpty(directory); + string configPath = Path.Combine(directory, ConfigFileName); + + List elements = default!; + try + { + using (FileStream readStream = File.OpenRead(configPath)) + { + elements = IniSerializer.Deserialize(readStream).ToList(); + } + } + catch (FileNotFoundException ex) + { + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); + } + catch (DirectoryNotFoundException ex) + { + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); + } + catch (UnauthorizedAccessException ex) + { + ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex); + } + + bool changed = false; + + foreach (IniElement element in elements) + { + if (element is IniParameter parameter) + { + if (parameter.Key == "channel") + { + changed = parameter.Set(scheme.Channel.ToString("D")) || changed; + } + + if (parameter.Key == "sub_channel") + { + changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; + } + } + } + + if (changed) + { + using (FileStream writeStream = File.Create(configPath)) + { + IniSerializer.Serialize(writeStream, elements); + } + } + + return changed; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs new file mode 100644 index 00000000..a07fbc9a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Scheme; + +namespace Snap.Hutao.Service.Game.Configuration; + +internal interface IGameChannelOptionsService +{ + ChannelOptions GetChannelOptions(); + + bool SetChannelOptions(LaunchScheme scheme); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IgnoredInvalidChannelOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IgnoredInvalidChannelOptions.cs similarity index 91% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IgnoredInvalidChannelOptions.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IgnoredInvalidChannelOptions.cs index eac6d975..1d0e6266 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IgnoredInvalidChannelOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IgnoredInvalidChannelOptions.cs @@ -4,7 +4,7 @@ using Snap.Hutao.Model.Intrinsic; using System.Collections.Immutable; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Configuration; internal static class IgnoredInvalidChannelOptions { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs new file mode 100644 index 00000000..7f773c77 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs @@ -0,0 +1,60 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Locator; + +namespace Snap.Hutao.Service.Game; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGamePathService))] +internal sealed partial class GamePathService : IGamePathService +{ + private readonly IServiceProvider serviceProvider; + private readonly AppOptions appOptions; + + public async ValueTask> SilentGetGamePathAsync() + { + // Cannot find in setting + if (string.IsNullOrEmpty(appOptions.GamePath)) + { + IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); + + bool isOk; + string path; + + // Try locate by unity log + (isOk, path) = await locatorFactory + .Create(GameLocationSource.UnityLog) + .LocateGamePathAsync() + .ConfigureAwait(false); + + if (!isOk) + { + // Try locate by registry + (isOk, path) = await locatorFactory + .Create(GameLocationSource.Registry) + .LocateGamePathAsync() + .ConfigureAwait(false); + } + + if (isOk) + { + // Save result. + appOptions.GamePath = path; + } + else + { + return new(false, SH.ServiceGamePathLocateFailed); + } + } + + if (!string.IsNullOrEmpty(appOptions.GamePath)) + { + return new(true, appOptions.GamePath); + } + else + { + return new(false, default!); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs new file mode 100644 index 00000000..dc3e07ec --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs @@ -0,0 +1,128 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using Snap.Hutao.Service.Game.Unlocker; +using System.Diagnostics; +using System.IO; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game; + +/// +/// 进程互操作 +/// +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameProcessService))] +internal sealed partial class GameProcessService : IGameProcessService +{ + private readonly IServiceProvider serviceProvider; + private readonly RuntimeOptions runtimeOptions; + private readonly LaunchOptions launchOptions; + private readonly AppOptions appOptions; + + private volatile int runningGamesCounter; + + public bool IsGameRunning() + { + if (runningGamesCounter == 0) + { + return false; + } + + return Process.GetProcessesByName(YuanShenProcessName).Any() + || Process.GetProcessesByName(GenshinImpactProcessName).Any(); + } + + public async ValueTask LaunchAsync(IProgress progress) + { + if (IsGameRunning()) + { + return; + } + + string gamePath = appOptions.GamePath; + ArgumentException.ThrowIfNullOrEmpty(gamePath); + + progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); + using (Process game = InitializeGameProcess(launchOptions, gamePath)) + { + try + { + Interlocked.Increment(ref runningGamesCounter); + game.Start(); + progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); + + if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) + { + progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); + try + { + await UnlockFpsAsync(serviceProvider, game, progress).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + // The Unlocker can't unlock the process + game.Kill(); + throw; + } + finally + { + progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + } + } + else + { + progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); + await game.WaitForExitAsync().ConfigureAwait(false); + progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + } + } + finally + { + Interlocked.Decrement(ref runningGamesCounter); + } + } + } + + private static Process InitializeGameProcess(LaunchOptions options, string gamePath) + { + string commandLine = string.Empty; + + if (options.IsEnabled) + { + Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); + + // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html + // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html + commandLine = new CommandLineBuilder() + .AppendIf("-popupwindow", options.IsBorderless) + .AppendIf("-window-mode", options.IsExclusive, "exclusive") + .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) + .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) + .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) + .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) + .ToString(); + } + + return new() + { + StartInfo = new() + { + Arguments = commandLine, + FileName = gamePath, + UseShellExecute = true, + Verb = "runas", + WorkingDirectory = Path.GetDirectoryName(gamePath), + }, + }; + } + + private static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress progress, CancellationToken token = default) + { + IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); + UnlockTimingOptions options = new(100, 20000, 3000); + Progress lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); + return unlocker.UnlockAsync(options, lockerProgress, token); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs deleted file mode 100644 index 25ff0bcd..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core; -using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Core.IO.Ini; -using Snap.Hutao.Factory.Abstraction; -using Snap.Hutao.Model.Entity; -using Snap.Hutao.Service.Game.Locator; -using Snap.Hutao.Service.Game.Package; -using Snap.Hutao.View.Dialog; -using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; -using Snap.Hutao.Web.Response; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using static Snap.Hutao.Service.Game.GameConstants; - -namespace Snap.Hutao.Service.Game; - -/// -/// 游戏服务 -/// -[HighQuality] -[ConstructorGenerated] -[Injection(InjectAs.Singleton, typeof(IGameService))] -internal sealed partial class GameService : IGameService -{ - private readonly IContentDialogFactory contentDialogFactory; - private readonly PackageConverter packageConverter; - private readonly IServiceProvider serviceProvider; - private readonly IGameDbService gameDbService; - private readonly LaunchOptions launchOptions; - private readonly RuntimeOptions runtimeOptions; - private readonly ITaskContext taskContext; - private readonly AppOptions appOptions; - - private volatile int runningGamesCounter; - private ObservableCollection? gameAccounts; - - /// - public ObservableCollection GameAccountCollection - { - get => gameAccounts ??= gameDbService.GetGameAccountCollection(); - } - - /// - public async ValueTask> GetGamePathAsync() - { - // Cannot find in setting - if (string.IsNullOrEmpty(appOptions.GamePath)) - { - IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); - - // Try locate by unity log - ValueResult result = await locatorFactory - .Create(GameLocationSource.UnityLog) - .LocateGamePathAsync() - .ConfigureAwait(false); - - if (!result.IsOk) - { - // Try locate by registry - result = await locatorFactory - .Create(GameLocationSource.Registry) - .LocateGamePathAsync() - .ConfigureAwait(false); - } - - if (result.IsOk) - { - // Save result. - appOptions.GamePath = result.Value; - } - else - { - return new(false, SH.ServiceGamePathLocateFailed); - } - } - - if (!string.IsNullOrEmpty(appOptions.GamePath)) - { - return new(true, appOptions.GamePath); - } - else - { - return new(false, default!); - } - } - - /// - public ChannelOptions GetChannelOptions() - { - string gamePath = appOptions.GamePath; - string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); - bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); - - if (!File.Exists(configPath)) - { - return ChannelOptions.FileNotFound(isOversea, configPath); - } - - using (FileStream stream = File.OpenRead(configPath)) - { - List parameters = IniSerializer.Deserialize(stream).OfType().ToList(); - string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value; - string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value; - - return new(channel, subChannel, isOversea); - } - } - - /// - public bool SetChannelOptions(LaunchScheme scheme) - { - string gamePath = appOptions.GamePath; - string? directory = Path.GetDirectoryName(gamePath); - ArgumentException.ThrowIfNullOrEmpty(directory); - string configPath = Path.Combine(directory, ConfigFileName); - - List elements = default!; - try - { - using (FileStream readStream = File.OpenRead(configPath)) - { - elements = IniSerializer.Deserialize(readStream).ToList(); - } - } - catch (FileNotFoundException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex); - } - catch (UnauthorizedAccessException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex); - } - - bool changed = false; - - foreach (IniElement element in elements) - { - if (element is IniParameter parameter) - { - if (parameter.Key == "channel") - { - changed = parameter.Set(scheme.Channel.ToString("D")) || changed; - } - - if (parameter.Key == "sub_channel") - { - changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; - } - } - } - - if (changed) - { - using (FileStream writeStream = File.Create(configPath)) - { - IniSerializer.Serialize(writeStream, elements); - } - } - - return changed; - } - - /// - public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) - { - string gamePath = appOptions.GamePath; - string? gameFolder = Path.GetDirectoryName(gamePath); - ArgumentException.ThrowIfNullOrEmpty(gameFolder); - string gameFileName = Path.GetFileName(gamePath); - - progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); - Response response = await serviceProvider - .GetRequiredService() - .GetResourceAsync(launchScheme) - .ConfigureAwait(false); - - if (response.IsOk()) - { - GameResource resource = response.Data; - - if (!launchScheme.ExecutableMatches(gameFileName)) - { - bool replaced = await packageConverter - .EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress) - .ConfigureAwait(false); - - if (replaced) - { - // We need to change the gamePath if we switched. - string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; - - await taskContext.SwitchToMainThreadAsync(); - appOptions.GamePath = Path.Combine(gameFolder, exeName); - } - else - { - // We can't start the game - // when we failed to convert game - return false; - } - } - - await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); - - return true; - } - - return false; - } - - /// - public bool IsGameRunning() - { - if (runningGamesCounter == 0) - { - return false; - } - - return Process.GetProcessesByName(YuanShenProcessName).Any() - || Process.GetProcessesByName(GenshinImpactProcessName).Any(); - } - - /// - public async ValueTask LaunchAsync(IProgress progress) - { - if (IsGameRunning()) - { - return; - } - - string gamePath = appOptions.GamePath; - ArgumentException.ThrowIfNullOrEmpty(gamePath); - - progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); - using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath)) - { - try - { - Interlocked.Increment(ref runningGamesCounter); - game.Start(); - progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); - - if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) - { - progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); - try - { - await ProcessInterop.UnlockFpsAsync(serviceProvider, game, progress).ConfigureAwait(false); - } - catch (InvalidOperationException) - { - // The Unlocker can't unlock the process - game.Kill(); - throw; - } - finally - { - progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); - } - } - else - { - progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); - await game.WaitForExitAsync().ConfigureAwait(false); - progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); - } - } - finally - { - Interlocked.Decrement(ref runningGamesCounter); - } - } - } - - /// - public async ValueTask DetectGameAccountAsync() - { - ArgumentNullException.ThrowIfNull(gameAccounts); - - string? registrySdk = RegistryInterop.Get(); - if (!string.IsNullOrEmpty(registrySdk)) - { - GameAccount? account = null; - try - { - account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); - } - - if (account is null) - { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); - - if (isOk) - { - account = GameAccount.From(name, registrySdk); - - // sync database - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); - - // sync cache - await taskContext.SwitchToMainThreadAsync(); - gameAccounts.Add(account); - } - } - - return account; - } - - return default; - } - - /// - public GameAccount? DetectCurrentGameAccount() - { - ArgumentNullException.ThrowIfNull(gameAccounts); - - string? registrySdk = RegistryInterop.Get(); - - if (!string.IsNullOrEmpty(registrySdk)) - { - try - { - return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); - } - } - - return null; - } - - /// - public bool SetGameAccount(GameAccount account) - { - if (string.IsNullOrEmpty(appOptions.PowerShellPath)) - { - ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!); - } - - return RegistryInterop.Set(account, appOptions.PowerShellPath); - } - - /// - public void AttachGameAccountToUid(GameAccount gameAccount, string uid) - { - gameAccount.UpdateAttachUid(uid); - gameDbService.UpdateGameAccount(gameAccount); - } - - /// - public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) - { - await taskContext.SwitchToMainThreadAsync(); - LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance(); - (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true); - - if (isOk) - { - gameAccount.UpdateName(name); - - // sync database - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false); - } - } - - /// - public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) - { - await taskContext.SwitchToMainThreadAsync(); - ArgumentNullException.ThrowIfNull(gameAccounts); - gameAccounts.Remove(gameAccount); - - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs new file mode 100644 index 00000000..0de8977b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -0,0 +1,104 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Game.Account; +using Snap.Hutao.Service.Game.Configuration; +using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Scheme; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.Game; + +/// +/// 游戏服务 +/// +[HighQuality] +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGameServiceFacade))] +internal sealed partial class GameServiceFacade : IGameServiceFacade +{ + private readonly IGameChannelOptionsService gameChannelOptionsService; + private readonly IGameAccountService gameAccountService; + private readonly IGameProcessService gameProcessService; + private readonly IGamePackageService gamePackageService; + private readonly IGamePathService gamePathService; + + /// + public ObservableCollection GameAccountCollection + { + get => gameAccountService.GameAccountCollection; + } + + /// + public ValueTask> GetGamePathAsync() + { + return gamePathService.SilentGetGamePathAsync(); + } + + /// + public ChannelOptions GetChannelOptions() + { + return gameChannelOptionsService.GetChannelOptions(); + } + + /// + public bool SetChannelOptions(LaunchScheme scheme) + { + return gameChannelOptionsService.SetChannelOptions(scheme); + } + + /// + public ValueTask DetectGameAccountAsync() + { + return gameAccountService.DetectGameAccountAsync(); + } + + /// + public GameAccount? DetectCurrentGameAccount() + { + return gameAccountService.DetectCurrentGameAccount(); + } + + /// + public bool SetGameAccount(GameAccount account) + { + return gameAccountService.SetGameAccount(account); + } + + /// + public void AttachGameAccountToUid(GameAccount gameAccount, string uid) + { + gameAccountService.AttachGameAccountToUid(gameAccount, uid); + } + + /// + public ValueTask ModifyGameAccountAsync(GameAccount gameAccount) + { + return gameAccountService.ModifyGameAccountAsync(gameAccount); + } + + /// + public ValueTask RemoveGameAccountAsync(GameAccount gameAccount) + { + return gameAccountService.RemoveGameAccountAsync(gameAccount); + } + + /// + public bool IsGameRunning() + { + return gameProcessService.IsGameRunning(); + } + + /// + public ValueTask LaunchAsync(IProgress progress) + { + return gameProcessService.LaunchAsync(progress); + } + + /// + public ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + { + return gamePackageService.EnsureGameResourceAsync(launchScheme, progress); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs new file mode 100644 index 00000000..c0b09ccc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs @@ -0,0 +1,9 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game; + +internal interface IGamePathService +{ + ValueTask> SilentGetGamePathAsync(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs new file mode 100644 index 00000000..d2c08c07 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game; + +internal interface IGameProcessService +{ + bool IsGameRunning(); + + ValueTask LaunchAsync(IProgress progress); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs index 19ad9808..89528ef7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs @@ -2,7 +2,10 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Game.Account; +using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Scheme; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Game; @@ -11,7 +14,7 @@ namespace Snap.Hutao.Service.Game; /// 游戏服务 /// [HighQuality] -internal interface IGameService +internal interface IGameServiceFacade { /// /// 游戏内账号集合 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs index 62a6ea3b..df7e3c1c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchStatus.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Service.Game.Unlocker; + namespace Snap.Hutao.Service.Game; internal sealed class LaunchStatus @@ -14,4 +16,16 @@ internal sealed class LaunchStatus public LaunchPhase Phase { get; set; } public string Description { get; set; } -} + + public static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus) + { + if (unlockerStatus.FindModuleState == FindModuleResult.Ok) + { + return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed); + } + else + { + return new(LaunchPhase.UnlockFpsFailed, SH.ServiceGameLaunchPhaseUnlockFpsFailed); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs new file mode 100644 index 00000000..831e1c5c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -0,0 +1,67 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Scheme; +using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; +using Snap.Hutao.Web.Response; +using System.IO; +using static Snap.Hutao.Service.Game.GameConstants; + +namespace Snap.Hutao.Service.Game.Package; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IGamePackageService))] +internal sealed partial class GamePackageService : IGamePackageService +{ + private readonly PackageConverter packageConverter; + private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; + private readonly AppOptions appOptions; + + public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + { + string gamePath = appOptions.GamePath; + string? gameFolder = Path.GetDirectoryName(gamePath); + ArgumentException.ThrowIfNullOrEmpty(gameFolder); + string gameFileName = Path.GetFileName(gamePath); + + progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); + Response response = await serviceProvider + .GetRequiredService() + .GetResourceAsync(launchScheme) + .ConfigureAwait(false); + + if (response.IsOk()) + { + GameResource resource = response.Data; + + if (!launchScheme.ExecutableMatches(gameFileName)) + { + bool replaced = await packageConverter + .EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress) + .ConfigureAwait(false); + + if (replaced) + { + // We need to change the gamePath if we switched. + string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; + + await taskContext.SwitchToMainThreadAsync(); + appOptions.GamePath = Path.Combine(gameFolder, exeName); + } + else + { + // We can't start the game + // when we failed to convert game + return false; + } + } + + await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs new file mode 100644 index 00000000..ffb3d54b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Scheme; + +namespace Snap.Hutao.Service.Game.Package; + +internal interface IGamePackageService +{ + ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); +} \ 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/PackageConverter.cs index a01a8f22..3e39bfd9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -6,6 +6,7 @@ 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.Service.Game.Scheme; using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; using System.IO; using System.IO.Compression; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs deleted file mode 100644 index 3b3dc973..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core; -using Snap.Hutao.Service.Game.Unlocker; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using Windows.Win32.Foundation; -using Windows.Win32.System.Memory; -using Windows.Win32.System.Threading; -using static Windows.Win32.PInvoke; - -namespace Snap.Hutao.Service.Game; - -/// -/// 进程互操作 -/// -internal static class ProcessInterop -{ - /// - /// 获取初始化后的游戏进程 - /// - /// 启动选项 - /// 游戏路径 - /// 初始化后的游戏进程 - public static Process InitializeGameProcess(LaunchOptions options, string gamePath) - { - string commandLine = string.Empty; - - if (options.IsEnabled) - { - Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); - - // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html - // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html - commandLine = new CommandLineBuilder() - .AppendIf("-popupwindow", options.IsBorderless) - .AppendIf("-window-mode", options.IsExclusive, "exclusive") - .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) - .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) - .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) - .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) - .ToString(); - } - - return new() - { - StartInfo = new() - { - Arguments = commandLine, - FileName = gamePath, - UseShellExecute = true, - Verb = "runas", - WorkingDirectory = Path.GetDirectoryName(gamePath), - }, - }; - } - - public static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress progress, CancellationToken token = default) - { - IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); - UnlockTimingOptions options = new(100, 20000, 3000); - Progress lockerProgress = new(unlockStatus => progress.Report(FromUnlockStatus(unlockStatus))); - return unlocker.UnlockAsync(options, lockerProgress, token); - } - - /// - /// 尝试禁用mhypbase - /// - /// 游戏进程 - /// 游戏路径 - /// 是否禁用成功 - public static bool DisableProtection(Process game, string gamePath) - { - string? gameFolder = Path.GetDirectoryName(gamePath); - string mhypbaseDll = Path.Combine(gameFolder ?? string.Empty, "mhypbase.dll"); - - if (File.Exists(mhypbaseDll)) - { - using (File.OpenHandle(mhypbaseDll, share: FileShare.None)) - { - SpinWait.SpinUntil(() => game.MainWindowHandle != 0); - return true; - } - } - - return false; - } - - /// - /// 加载并注入指定路径的库 - /// - /// 进程句柄 - /// 库的路径,不包含'\0' - public static unsafe void LoadLibraryAndInject(in HANDLE hProcess, in ReadOnlySpan libraryPathu8) - { - HINSTANCE hKernelDll = GetModuleHandle("kernel32.dll"); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - FARPROC pLoadLibraryA = GetProcAddress(hKernelDll, "LoadLibraryA"u8); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - void* pNativeLibraryPath = default; - try - { - VIRTUAL_ALLOCATION_TYPE allocType = VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT; - pNativeLibraryPath = VirtualAllocEx(hProcess, default, unchecked((uint)libraryPathu8.Length + 1), allocType, PAGE_PROTECTION_FLAGS.PAGE_READWRITE); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - WriteProcessMemory(hProcess, pNativeLibraryPath, libraryPathu8); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - LPTHREAD_START_ROUTINE lpThreadLoadLibraryA = pLoadLibraryA.CreateDelegate(); - HANDLE hLoadLibraryAThread = default; - try - { - hLoadLibraryAThread = CreateRemoteThread(hProcess, default, 0, lpThreadLoadLibraryA, pNativeLibraryPath, 0); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - - WaitForSingleObject(hLoadLibraryAThread, 2000); - Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError()); - } - finally - { - CloseHandle(hLoadLibraryAThread); - } - } - finally - { - VirtualFreeEx(hProcess, pNativeLibraryPath, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE); - } - } - - private static unsafe FARPROC GetProcAddress(in HINSTANCE hModule, in ReadOnlySpan lpProcName) - { - fixed (byte* lpProcNameLocal = lpProcName) - { - return Windows.Win32.PInvoke.GetProcAddress(hModule, new PCSTR(lpProcNameLocal)); - } - } - - private static unsafe BOOL WriteProcessMemory(in HANDLE hProcess, void* lpBaseAddress, in ReadOnlySpan buffer) - { - fixed (void* lpBuffer = buffer) - { - return Windows.Win32.PInvoke.WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, unchecked((uint)buffer.Length)); - } - } - - private static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus) - { - if (unlockerStatus.FindModuleState == FindModuleResult.Ok) - { - return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed); - } - else - { - return new(LaunchPhase.UnlockFpsFailed, SH.ServiceGameLaunchPhaseUnlockFpsFailed); - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/KnownLaunchSchemes.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/KnownLaunchSchemes.cs similarity index 98% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/KnownLaunchSchemes.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/KnownLaunchSchemes.cs index 16fac365..c1373917 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/KnownLaunchSchemes.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/KnownLaunchSchemes.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal static class KnownLaunchSchemes { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs index 5aeede35..69e5698a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchScheme.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs @@ -2,14 +2,15 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Service.Game.Configuration; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; /// /// 启动方案 /// [HighQuality] -internal partial class LaunchScheme +internal class LaunchScheme { /// /// 显示名称 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeBilibili.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeBilibili.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeBilibili.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeBilibili.cs index cd5057be..1d96e21d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeBilibili.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeBilibili.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal sealed class LaunchSchemeBilibili : LaunchScheme { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeChinese.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeChinese.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeChinese.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeChinese.cs index 1e8f0e9f..e9d27c6d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeChinese.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeChinese.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal sealed class LaunchSchemeChinese : LaunchScheme { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeOversea.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeOversea.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeOversea.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeOversea.cs index 2838d82e..08b6799a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchSchemeOversea.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchSchemeOversea.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Model.Intrinsic; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Scheme; internal sealed class LaunchSchemeOversea : LaunchScheme { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml index 0f12787f..4b720db2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml @@ -382,7 +382,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/> @@ -391,7 +391,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/> @@ -405,7 +405,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/> @@ -414,7 +414,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/> @@ -423,7 +423,7 @@ Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewControlStatisticsCardBlueText}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 24dc9524..6b5704c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -9,7 +9,9 @@ using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service; using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; @@ -42,7 +44,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private readonly RuntimeOptions hutaoOptions; private readonly IUserService userService; private readonly ITaskContext taskContext; - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly IMemoryCache memoryCache; private readonly AppOptions appOptions; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index c04150b2..aba0fd04 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -16,7 +16,7 @@ namespace Snap.Hutao.ViewModel.Game; [ConstructorGenerated(CallBaseConstructor = true)] internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim { - private readonly IGameService gameService; + private readonly IGameServiceFacade gameService; private readonly ITaskContext taskContext; private readonly IInfoBarService infoBarService; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 4247f148..2f1f87c4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Primitive; -using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Hoyolab; namespace Snap.Hutao.Web; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs index 159bb4f4..676fb754 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Primitive; -using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Hoyolab; namespace Snap.Hutao.Web; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs index c7a319e5..738d0d08 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; using Snap.Hutao.Web.Response; From e4e9dd91f17e2ce90f99009258977c8a6f84986d Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Sat, 4 Nov 2023 17:21:31 +0800 Subject: [PATCH 10/24] impl #1062 --- .../Snap.Hutao/Resource/Localization/SH.resx | 6 + .../Snap.Hutao/Service/Game/LaunchOptions.cs | 21 ++ .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 190 +++++++++--------- 3 files changed, 126 insertions(+), 91 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 27e1b0e2..5f1db962 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1967,6 +1967,12 @@ 高级功能 + + 快速切换到指定的分辨率 + + + 分辨率 + 将窗口创建为弹出窗口,不带框架 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index ac514a31..74c72f2b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; @@ -35,6 +36,7 @@ internal sealed class LaunchOptions : DbStoreOptions private int? targetFps; private NameValue? monitor; private bool? isMonitorEnabled; + private AspectRatio? selectedAspectRatio; /// /// 构造一个新的启动游戏选项 @@ -162,6 +164,25 @@ internal sealed class LaunchOptions : DbStoreOptions set => SetOption(ref isMonitorEnabled, SettingEntry.LaunchIsMonitorEnabled, value); } + public List AspectRatios { get; } = new() + { + new(2560, 1440), + new(1920, 1080), + }; + + public AspectRatio? SelectedAspectRatio + { + get => selectedAspectRatio; + set + { + if (SetProperty(ref selectedAspectRatio, value) && value is AspectRatio aspectRatio) + { + ScreenWidth = (int)aspectRatio.Width; + ScreenHeight = (int)aspectRatio.Height; + } + } + } + private static void InitializeMonitors(List> monitors) { // This list can't use foreach diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index fa9098e9..87a87fb2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -34,6 +34,82 @@ DataContext="{Binding Mode=OneWay}" Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceDiffHeader}"/> + + + + + + + + + internal interface ITaskContext { - IProgress CreateProgressForMainThread(Action handler); + SynchronizationContext GetSynchronizationContext(); /// /// 在主线程上同步等待执行操作 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs index d8aee9cd..44d97e0e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/TaskContext.cs @@ -42,8 +42,8 @@ internal sealed class TaskContext : ITaskContext dispatcherQueue.Invoke(action); } - public IProgress CreateProgressForMainThread(Action handler) + public SynchronizationContext GetSynchronizationContext() { - return new DispatcherQueueProgress(handler, synchronizationContext); + return synchronizationContext; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs similarity index 84% rename from src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs index d71f0964..ccc736c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/ContentDialogFactory.cs @@ -3,9 +3,8 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Core.LifeCycle; -using Snap.Hutao.Factory.Abstraction; -namespace Snap.Hutao.Factory; +namespace Snap.Hutao.Factory.ContentDialog; /// [HighQuality] @@ -13,15 +12,15 @@ namespace Snap.Hutao.Factory; [Injection(InjectAs.Singleton, typeof(IContentDialogFactory))] internal sealed partial class ContentDialogFactory : IContentDialogFactory { + private readonly ICurrentWindowReference currentWindowReference; private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; - private readonly ICurrentWindowReference currentWindowReference; /// public async ValueTask CreateForConfirmAsync(string title, string content) { await taskContext.SwitchToMainThreadAsync(); - ContentDialog dialog = new() + Microsoft.UI.Xaml.Controls.ContentDialog dialog = new() { XamlRoot = currentWindowReference.GetXamlRoot(), Title = title, @@ -37,7 +36,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory public async ValueTask CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close) { await taskContext.SwitchToMainThreadAsync(); - ContentDialog dialog = new() + Microsoft.UI.Xaml.Controls.ContentDialog dialog = new() { XamlRoot = currentWindowReference.GetXamlRoot(), Title = title, @@ -51,10 +50,10 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory } /// - public async ValueTask CreateForIndeterminateProgressAsync(string title) + public async ValueTask CreateForIndeterminateProgressAsync(string title) { await taskContext.SwitchToMainThreadAsync(); - ContentDialog dialog = new() + Microsoft.UI.Xaml.Controls.ContentDialog dialog = new() { XamlRoot = currentWindowReference.GetXamlRoot(), Title = title, @@ -65,7 +64,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory } public async ValueTask CreateInstanceAsync(params object[] parameters) - where TContentDialog : ContentDialog + where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog { await taskContext.SwitchToMainThreadAsync(); TContentDialog contentDialog = serviceProvider.CreateInstance(parameters); @@ -74,7 +73,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory } public TContentDialog CreateInstance(params object[] parameters) - where TContentDialog : ContentDialog + where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog { TContentDialog contentDialog = serviceProvider.CreateInstance(parameters); contentDialog.XamlRoot = currentWindowReference.GetXamlRoot(); diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs index b09c2f6f..00c434c3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IContentDialogFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/ContentDialog/IContentDialogFactory.cs @@ -3,7 +3,7 @@ using Microsoft.UI.Xaml.Controls; -namespace Snap.Hutao.Factory.Abstraction; +namespace Snap.Hutao.Factory.ContentDialog; /// /// 内容对话框工厂 @@ -33,11 +33,11 @@ internal interface IContentDialogFactory /// /// 标题 /// 内容对话框 - ValueTask CreateForIndeterminateProgressAsync(string title); + ValueTask CreateForIndeterminateProgressAsync(string title); TContentDialog CreateInstance(params object[] parameters) - where TContentDialog : ContentDialog; + where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog; ValueTask CreateInstanceAsync(params object[] parameters) - where TContentDialog : ContentDialog; + where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs similarity index 97% rename from src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs index 7252d9c0..353c6146 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Abstraction/IPickerFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs @@ -3,7 +3,7 @@ using Windows.Storage.Pickers; -namespace Snap.Hutao.Factory.Abstraction; +namespace Snap.Hutao.Factory.Picker; /// /// 文件选择器工厂 diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs similarity index 85% rename from src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs index bd177eb6..4000235b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/PickerFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs @@ -4,11 +4,10 @@ using Snap.Hutao.Core; using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.Windowing; -using Snap.Hutao.Factory.Abstraction; using Windows.Storage.Pickers; using WinRT.Interop; -namespace Snap.Hutao.Factory; +namespace Snap.Hutao.Factory.Picker; /// [HighQuality] @@ -18,7 +17,7 @@ internal sealed partial class PickerFactory : IPickerFactory { private const string AnyType = "*"; - private readonly ICurrentWindowReference currentWindow; + private readonly ICurrentWindowReference currentWindowReference; /// public FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes) @@ -80,10 +79,11 @@ internal sealed partial class PickerFactory : IPickerFactory { // Create a folder picker. T picker = new(); - if (currentWindow.Window is IWindowOptionsSource optionsSource) - { - InitializeWithWindow.Initialize(picker, optionsSource.WindowOptions.Hwnd); - } + nint hwnd = currentWindowReference.Window is IWindowOptionsSource optionsSource + ? (nint)optionsSource.WindowOptions.Hwnd + : WindowNative.GetWindowHandle(currentWindowReference.Window); + + InitializeWithWindow.Initialize(picker, hwnd); return picker; } diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Progress/IProgressFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/IProgressFactory.cs new file mode 100644 index 00000000..016c2235 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/IProgressFactory.cs @@ -0,0 +1,6 @@ +namespace Snap.Hutao.Factory.Progress; + +internal interface IProgressFactory +{ + IProgress CreateForMainThread(Action handler); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs new file mode 100644 index 00000000..197845e9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Progress/ProgressFactory.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snap.Hutao.Factory.Progress; + +[ConstructorGenerated] +[Injection(InjectAs.Transient, typeof(IProgressFactory))] +internal sealed partial class ProgressFactory : IProgressFactory +{ + private readonly ITaskContext taskContext; + + public IProgress CreateForMainThread(Action handler) + { + return new DispatcherQueueProgress(handler, taskContext.GetSynchronizationContext()); + } +} \ No newline at end of file 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 4c0b1799..b6e00850 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -104,6 +104,8 @@ internal sealed partial class SettingEntry public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled"; + public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics"; + /// /// 启动游戏 多倍启动 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 5f1db962..643e811e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2021,6 +2021,9 @@ 文件 + + 进程间 + 在指定的显示器上运行 @@ -2036,6 +2039,12 @@ 游戏选项 + + 在游戏启动后尝试启动并使用 Starward 进行游戏时长统计 + + + 时长统计 + 进程 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index b51db023..3e9834e3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -106,6 +106,8 @@ internal sealed partial class AppOptions : DbStoreOptions /// /// 是否启用高级功能 + /// DO NOT MOVE TO OTHER CLASS + /// We are binding this property in SettingPage /// public bool IsAdvancedLaunchOptionsEnabled { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs new file mode 100644 index 00000000..07fccf2f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.IO; + +namespace Snap.Hutao.Service; + +internal static class AppOptionsExtension +{ + public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName) + { + string gamePath = appOptions.GamePath; + + gameFolder = Path.GetDirectoryName(gamePath); + if (string.IsNullOrEmpty(gameFolder)) + { + gameFileName = default; + return false; + } + + gameFileName = Path.GetFileName(gamePath); + if (string.IsNullOrEmpty(gameFileName)) + { + return false; + } + + return true; + } + + public static bool TryGetGameFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFileName) + { + string gamePath = appOptions.GamePath; + + gameFileName = Path.GetFileName(gamePath); + if (string.IsNullOrEmpty(gameFileName)) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index 9c797a72..2a2bbc38 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Service.Metadata; using Snap.Hutao.View.Dialog; using Snap.Hutao.Web.Request.QueryString; 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 ff717fab..bc5884ca 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; @@ -45,8 +45,6 @@ internal sealed partial class GameAccountService : IGameAccountService if (account is null) { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs index 29e8d129..513fb4a2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs @@ -55,7 +55,7 @@ internal static class RegistryInterop try { - Process.Start(startInfo)?.WaitForExit(); + System.Diagnostics.Process.Start(startInfo)?.WaitForExit(); } catch (Win32Exception ex) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs index 0de8977b..cb20776b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -5,6 +5,7 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game.Account; using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Process; using Snap.Hutao.Service.Game.Scheme; using System.Collections.ObjectModel; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 74c72f2b..28d1e743 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -37,6 +37,7 @@ internal sealed class LaunchOptions : DbStoreOptions private NameValue? monitor; private bool? isMonitorEnabled; private AspectRatio? selectedAspectRatio; + private bool? useStarwardPlayTimeStatistics; /// /// 构造一个新的启动游戏选项 @@ -183,6 +184,12 @@ internal sealed class LaunchOptions : DbStoreOptions } } + public bool UseStarwardPlayTimeStatistics + { + get => GetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, false); + set => SetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, value); + } + private static void InitializeMonitors(List> monitors) { // This list can't use foreach diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs index 7f281662..8e8ec8db 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core.IO; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.Picker; using Windows.Storage.Pickers; namespace Snap.Hutao.Service.Game.Locator; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs index 831e1c5c..77b99c2b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -20,10 +20,10 @@ internal sealed partial class GamePackageService : IGamePackageService public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { - string gamePath = appOptions.GamePath; - string? gameFolder = Path.GetDirectoryName(gamePath); - ArgumentException.ThrowIfNullOrEmpty(gameFolder); - string gameFileName = Path.GetFileName(gamePath); + if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName)) + { + return false; + } progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); Response response = await serviceProvider diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs similarity index 67% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index dc3e07ec..46d51181 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -2,12 +2,13 @@ // Licensed under the MIT license. using Snap.Hutao.Core; +using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Game.Unlocker; -using System.Diagnostics; using System.IO; +using Windows.System; using static Snap.Hutao.Service.Game.GameConstants; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Process; /// /// 进程互操作 @@ -30,8 +31,8 @@ internal sealed partial class GameProcessService : IGameProcessService return false; } - return Process.GetProcessesByName(YuanShenProcessName).Any() - || Process.GetProcessesByName(GenshinImpactProcessName).Any(); + return System.Diagnostics.Process.GetProcessesByName(YuanShenProcessName).Any() + || System.Diagnostics.Process.GetProcessesByName(GenshinImpactProcessName).Any(); } public async ValueTask LaunchAsync(IProgress progress) @@ -45,7 +46,7 @@ internal sealed partial class GameProcessService : IGameProcessService ArgumentException.ThrowIfNullOrEmpty(gamePath); progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); - using (Process game = InitializeGameProcess(launchOptions, gamePath)) + using (System.Diagnostics.Process game = InitializeGameProcess(gamePath)) { try { @@ -53,12 +54,18 @@ internal sealed partial class GameProcessService : IGameProcessService game.Start(); progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); + if (launchOptions.UseStarwardPlayTimeStatistics && appOptions.TryGetGameFileName(out string? gameFileName)) + { + bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName); + await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false); + } + if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) { progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); try { - await UnlockFpsAsync(serviceProvider, game, progress).ConfigureAwait(false); + await UnlockFpsAsync(game, progress).ConfigureAwait(false); } catch (InvalidOperationException) { @@ -85,23 +92,23 @@ internal sealed partial class GameProcessService : IGameProcessService } } - private static Process InitializeGameProcess(LaunchOptions options, string gamePath) + private System.Diagnostics.Process InitializeGameProcess(string gamePath) { string commandLine = string.Empty; - if (options.IsEnabled) + if (launchOptions.IsEnabled) { - Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效"); + Must.Argument(!(launchOptions.IsBorderless && launchOptions.IsExclusive), "无边框与独占全屏选项无法同时生效"); // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html commandLine = new CommandLineBuilder() - .AppendIf("-popupwindow", options.IsBorderless) - .AppendIf("-window-mode", options.IsExclusive, "exclusive") - .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) - .AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth) - .AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight) - .AppendIf("-monitor", options.IsMonitorEnabled, options.Monitor.Value) + .AppendIf("-popupwindow", launchOptions.IsBorderless) + .AppendIf("-window-mode", launchOptions.IsExclusive, "exclusive") + .Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0) + .AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth) + .AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight) + .AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value) .ToString(); } @@ -118,7 +125,7 @@ internal sealed partial class GameProcessService : IGameProcessService }; } - private static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress progress, CancellationToken token = default) + private ValueTask UnlockFpsAsync(System.Diagnostics.Process game, IProgress progress, CancellationToken token = default) { IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); UnlockTimingOptions options = new(100, 20000, 3000); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs similarity index 83% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs index d2c08c07..2f39d442 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.Process; internal interface IGameProcessService { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs new file mode 100644 index 00000000..1a827666 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.System; + +namespace Snap.Hutao.Service.Game.Process; + +internal static class Starward +{ + public static async ValueTask LaunchForPlayTimeStatisticsAsync(bool isOversea) + { + string gameBiz = isOversea ? "hk4e_global" : "hk4e_cn"; + Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri(); + if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available) + { + await Launcher.LaunchUriAsync(starwardPlayTimeUri); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs index 69e5698a..f2bf764f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs @@ -57,6 +57,16 @@ internal class LaunchScheme public bool IsNotCompatOnly { get; private protected set; } = true; + public static bool ExecutableIsOversea(string gameFileName) + { + return gameFileName switch + { + GameConstants.GenshinImpactFileName => true, + GameConstants.YuanShenFileName => false, + _ => throw Requires.Fail("无效的游戏可执行文件名称:{0}", gameFileName), + }; + } + /// /// 多通道相等 /// 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 21bba708..e36f515f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Game.Unlocker; [HighQuality] internal sealed class GameFpsUnlocker : IGameFpsUnlocker { - private readonly Process gameProcess; + private readonly System.Diagnostics.Process gameProcess; private readonly LaunchOptions launchOptions; private readonly UnlockerStatus status = new(); @@ -33,7 +33,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker /// /// 服务提供器 /// 游戏进程 - public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess) + public GameFpsUnlocker(IServiceProvider serviceProvider, System.Diagnostics.Process gameProcess) { launchOptions = serviceProvider.GetRequiredService(); this.gameProcess = gameProcess; @@ -57,7 +57,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker await LoopAdjustFpsAsync(options.AdjustFpsDelay, progress, token).ConfigureAwait(false); } - private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModule moduleEntryInfo, out VirtualMemory memory) + private static unsafe bool UnsafeReadModulesMemory(System.Diagnostics.Process process, in GameModule moduleEntryInfo, out VirtualMemory memory) { ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer; ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly; @@ -68,7 +68,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker && ReadProcessMemory((HANDLE)process.Handle, (void*)userAssembly.Address, lpBuffer + unityPlayer.Size, userAssembly.Size); } - private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value) + private static unsafe bool UnsafeReadProcessMemory(System.Diagnostics.Process process, nuint baseAddress, out nuint value) { ulong temp = 0; bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, (byte*)&temp, 8); @@ -78,7 +78,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker return result; } - private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int value) + private static unsafe bool UnsafeWriteProcessMemory(System.Diagnostics.Process process, nuint baseAddress, int value) { return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &value, sizeof(int)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs index 0f8579c7..dddc3381 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoAsAService.cs @@ -29,11 +29,11 @@ internal sealed partial class HutaoAsAService : IHutaoAsAService ApplicationDataCompositeValue excludedIds = LocalSetting.Get(SettingKeys.ExcludedAnnouncementIds, new ApplicationDataCompositeValue()); List data = excludedIds.Select(kvp => long.Parse(kvp.Key, CultureInfo.InvariantCulture)).ToList(); - Response> respose = await hutaoAsServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false); + Response> response = await hutaoAsServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false); - if (respose.IsOk()) + if (response.IsOk()) { - List list = respose.Data; + List list = response.Data; list.ForEach(item => item.DismissCommand = dismissCommand); announcements = list.ToObservableCollection(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 3b267b78..7ac0e919 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -277,7 +277,7 @@ - + all diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml index 0b2f163c..abdb0169 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml @@ -19,7 +19,7 @@ CompactPaneLength="48" IsBackEnabled="{x:Bind ContentFrame.CanGoBack, Mode=OneWay}" IsPaneOpen="True" - OpenPaneLength="188" + OpenPaneLength="192" PaneDisplayMode="Left" UseLayoutRounding="False"> diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 87a87fb2..80c2ca8c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -153,6 +153,7 @@ Message="{shcm:ResourceString Name=ViewPageLaunchGameConfigurationSaveHint}" Severity="Informational"/> + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs index b91fb822..e5a23ee0 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs @@ -5,7 +5,8 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Control.Extension; using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO.DataTransfer; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Factory.Picker; using Snap.Hutao.Model.InterChange.Achievement; using Snap.Hutao.Service.Achievement; using Snap.Hutao.Service.Notification; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index 8d51141a..35aecde4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -5,7 +5,8 @@ using CommunityToolkit.WinUI.Collections; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Core.IO; using Snap.Hutao.Core.LifeCycle; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Factory.Picker; using Snap.Hutao.Model.InterChange.Achievement; using Snap.Hutao.Service.Achievement; using Snap.Hutao.Service.Metadata; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index e4a67b0d..5f2181b5 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -8,7 +8,7 @@ using Microsoft.UI.Xaml.Media.Imaging; using Snap.Hutao.Control.Extension; using Snap.Hutao.Control.Media; using Snap.Hutao.Core.IO.DataTransfer; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Message; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs index 46883e09..90024897 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Metadata.Item; using Snap.Hutao.Model.Primitive; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs index 85e926c2..9ab591a2 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteViewModel.cs @@ -4,7 +4,7 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Control.Extension; using Snap.Hutao.Core; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.DailyNote; using Snap.Hutao.Service.Notification; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index 997e29fe..6dbf21d8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -6,7 +6,9 @@ using Snap.Hutao.Control.Extension; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.IO; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Factory.Picker; +using Snap.Hutao.Factory.Progress; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.InterChange.GachaLog; using Snap.Hutao.Service.GachaLog; @@ -27,10 +29,11 @@ namespace Snap.Hutao.ViewModel.GachaLog; [Injection(InjectAs.Scoped)] internal sealed partial class GachaLogViewModel : Abstraction.ViewModel { + private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel; private readonly IGachaLogQueryProviderFactory gachaLogQueryProviderFactory; private readonly IContentDialogFactory contentDialogFactory; - private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel; private readonly HutaoCloudViewModel hutaoCloudViewModel; + private readonly IProgressFactory progressFactory; private readonly IGachaLogService gachaLogService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; @@ -162,7 +165,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel throw; } - IProgress progress = taskContext.CreateProgressForMainThread(dialog.OnReport); + IProgress progress = progressFactory.CreateForMainThread(dialog.OnReport); bool authkeyValid; try diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs index 89a250e2..f7cc4040 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/HutaoCloudViewModel.cs @@ -3,7 +3,7 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Control.Extension; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.GachaLog; using Snap.Hutao.Service.Hutao; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 6b5704c2..4fd85dcf 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -5,7 +5,8 @@ using Microsoft.Extensions.Caching.Memory; using Snap.Hutao.Control.Extension; using Snap.Hutao.Core; using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Factory.Progress; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service; using Snap.Hutao.Service.Game; @@ -38,10 +39,11 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private readonly IContentDialogFactory contentDialogFactory; private readonly LaunchStatusOptions launchStatusOptions; private readonly INavigationService navigationService; + private readonly IProgressFactory progressFactory; private readonly IInfoBarService infoBarService; private readonly ResourceClient resourceClient; + private readonly RuntimeOptions runtimeOptions; private readonly LaunchOptions launchOptions; - private readonly RuntimeOptions hutaoOptions; private readonly IUserService userService; private readonly ITaskContext taskContext; private readonly IGameServiceFacade gameService; @@ -96,7 +98,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel /// /// 胡桃选项 /// - public RuntimeOptions HutaoOptions { get => hutaoOptions; } + public RuntimeOptions HutaoOptions { get => runtimeOptions; } /// /// 应用选项 @@ -191,45 +193,44 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel [Command("LaunchCommand")] private async Task LaunchAsync() { - if (SelectedScheme is not null) - { - try - { - gameService.SetChannelOptions(SelectedScheme); - - // Whether or not the channel options changed, we always ensure game resouces - LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - IProgress convertProgress = taskContext.CreateProgressForMainThread(state => dialog.State = state); - using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) - { - if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false)) - { - infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail); - return; - } - } - - if (SelectedGameAccount is not null) - { - if (!gameService.SetGameAccount(SelectedGameAccount)) - { - infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail); - return; - } - } - - IProgress launchProgress = taskContext.CreateProgressForMainThread(status => launchStatusOptions.LaunchStatus = status); - await gameService.LaunchAsync(launchProgress).ConfigureAwait(false); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(ExceptionFormat.Format(ex)); - infoBarService.Error(ex); - } - } - else + if (SelectedScheme is null) { infoBarService.Error(SH.ViewModelLaunchGameSchemeNotSelected); + return; + } + + try + { + // Always ensure game resources + gameService.SetChannelOptions(SelectedScheme); + + LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + IProgress convertProgress = progressFactory.CreateForMainThread(state => dialog.State = state); + + using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) + { + if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false)) + { + infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail); + return; + } + } + + if (SelectedGameAccount is not null) + { + if (!gameService.SetGameAccount(SelectedGameAccount)) + { + infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail); + return; + } + } + + IProgress launchProgress = progressFactory.CreateForMainThread(status => launchStatusOptions.LaunchStatus = status); + await gameService.LaunchAsync(launchProgress).ConfigureAwait(false); + } + catch (Exception ex) + { + infoBarService.Error(ex); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs index f0709654..28c619b3 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HutaoPassportViewModel.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core.Setting; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 523b6e96..131190c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -11,7 +11,8 @@ using Snap.Hutao.Core.IO.DataTransfer; using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Shell; using Snap.Hutao.Core.Windowing; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Factory.Picker; using Snap.Hutao.Model; using Snap.Hutao.Service; using Snap.Hutao.Service.GachaLog.QueryProvider; @@ -40,12 +41,12 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel { private readonly HomeCardOptions homeCardOptions = new(); + private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly IContentDialogFactory contentDialogFactory; private readonly IGameLocatorFactory gameLocatorFactory; private readonly INavigationService navigationService; private readonly IClipboardInterop clipboardInterop; private readonly IShellLinkInterop shellLinkInterop; - private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly HutaoUserOptions hutaoUserOptions; private readonly IInfoBarService infoBarService; private readonly RuntimeOptions runtimeOptions; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs index 96e4fcbe..41fb8a3d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs @@ -7,7 +7,7 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Snap.Hutao.Core; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.IO.DataTransfer; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.SignIn; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index b34762c1..d38ca327 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI.Collections; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Model.Intrinsic; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index d832ca06..dbdd95d8 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI.Collections; -using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Model.Intrinsic; From 2a4c93d2414df66019484ba689fe210a1bc95286 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 6 Nov 2023 11:17:16 +0800 Subject: [PATCH 13/24] impl #778 all --- .../Control/Panel/PanelSelector.xaml.cs | 45 ++++++++++++++----- .../Factory/TypedWishSummaryBuilder.cs | 1 + .../View/Control/StatisticsCard.xaml | 2 + .../Snap.Hutao/View/Page/AchievementPage.xaml | 5 ++- .../View/Page/AvatarPropertyPage.xaml | 5 ++- .../Snap.Hutao/View/Page/WikiAvatarPage.xaml | 5 ++- .../Snap.Hutao/View/Page/WikiMonsterPage.xaml | 5 ++- .../Snap.Hutao/View/Page/WikiWeaponPage.xaml | 5 ++- .../Snap.Hutao/ViewModel/GachaLog/Wish.cs | 5 +++ 9 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs index 266d2a77..8ce1ca54 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs @@ -3,6 +3,7 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; +using Snap.Hutao.Core.Setting; namespace Snap.Hutao.Control.Panel; @@ -11,6 +12,8 @@ namespace Snap.Hutao.Control.Panel; /// [HighQuality] [DependencyProperty("Current", typeof(string), List)] +[DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))] +[DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")] internal sealed partial class PanelSelector : Segmented { public const string List = nameof(List); @@ -42,21 +45,41 @@ internal sealed partial class PanelSelector : Segmented selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged); } - private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp) - { - Current = IndexTypeMap[(int)GetValue(dp)]; - } - - private void OnRootLoaded(object sender, RoutedEventArgs e) + private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp) { PanelSelector selector = (PanelSelector)sender; - selector.SelectedItem = selector.Items.Cast().Single(item => (string)item.Tag == Current); + selector.Current = IndexTypeMap[(int)selector.GetValue(dp)]; + + if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent)) + { + LocalSetting.Set(GetSettingKey(selector), selector.Current); + } } - private void OnRootUnload(object sender, RoutedEventArgs e) + private static void OnRootLoaded(object sender, RoutedEventArgs e) { - UnregisterPropertyChangedCallback(SelectedIndexProperty, selectedIndexChangedCallbackToken); - Loaded -= loadedEventHandler; - Unloaded -= unloadedEventHandler; + PanelSelector selector = (PanelSelector)sender; + + if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent)) + { + return; + } + + string value = LocalSetting.Get(GetSettingKey(selector), selector.Current); + selector.Current = value; + + selector.SelectedItem = selector.Items.Cast().Single(item => (string)item.Tag == selector.Current); + } + + private static void OnRootUnload(object sender, RoutedEventArgs e) + { + PanelSelector selector = (PanelSelector)sender; + selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken); + selector.Unloaded -= selector.unloadedEventHandler; + } + + private static string GetSettingKey(PanelSelector selector) + { + return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}"; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs index 917583f1..e6392eee 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs @@ -120,6 +120,7 @@ internal sealed class TypedWishSummaryBuilder { // base Name = context.Name, + TypeName = $"{context.DistributionType:D}", From = fromTimeTracker, To = toTimeTracker, TotalCount = totalCountTracker, diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml index 388f4d05..30b344c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml @@ -151,6 +151,8 @@ x:Name="ItemsPanelSelector" Margin="6,0,0,0" Current="Grid" + LocalSettingKeyExtraForCurrent="{Binding TypeName}" + LocalSettingKeySuffixForCurrent="StatisticsCard.OrangeList" Visibility="{x:Bind DetailExpander.IsExpanded, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index 1c4ddf73..cee08806 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -202,7 +202,10 @@ - + - + - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiMonsterPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiMonsterPage.xaml index 9624fac5..017ee457 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiMonsterPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiMonsterPage.xaml @@ -76,7 +76,10 @@ BorderThickness="0" DefaultLabelPosition="Right"> - + - + public string Name { get; set; } = default!; + /// + /// 类型名称,不受语言影响 + /// + public string? TypeName { get; set; } + /// /// 总数 /// From f41185310be987a81fa7d69a0a3b61992d08d6c0 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 6 Nov 2023 11:41:33 +0800 Subject: [PATCH 14/24] adjust wish typename --- .../Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs | 5 +++++ src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs index 4cdd8616..b74c20e2 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/TypedWishSummary.cs @@ -18,6 +18,11 @@ internal sealed partial class TypedWishSummary : Wish private double probabilityOfPredictedPullLeftToOrange; private double probabilityOfNextPullIsOrange; + /// + /// 类型名称,不受语言影响 + /// + public string? TypeName { get; set; } + /// /// 最大五星抽数 /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs index fed3f191..fc5278c0 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/Wish.cs @@ -14,11 +14,6 @@ internal abstract class Wish /// public string Name { get; set; } = default!; - /// - /// 类型名称,不受语言影响 - /// - public string? TypeName { get; set; } - /// /// 总数 /// From 075d92f754e576ac0651110c23631b50611a3bdc Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 6 Nov 2023 12:18:05 +0800 Subject: [PATCH 15/24] Set IsEnabled by a new property instead of setting it separately for each SettingsCard --- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 80c2ca8c..dfef36b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -12,6 +12,7 @@ xmlns:shc="using:Snap.Hutao.Control" xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector" + xmlns:shch="using:Snap.Hutao.Control.Helper" xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shvc="using:Snap.Hutao.View.Control" xmlns:shvg="using:Snap.Hutao.ViewModel.Game" @@ -202,25 +203,17 @@ Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}" HeaderIcon="{shcm:FontIcon Glyph=}" - IsExpanded="True"> + IsExpanded="True" + shch:SettingsExpanderHelper.IsItemsEnabled="{Binding Options.IsEnabled}"> - + - + - + @@ -230,10 +223,7 @@ ItemsSource="{Binding Options.AspectRatios}" SelectedItem="{Binding Options.SelectedAspectRatio, Mode=TwoWay}"/> - + - + - + Date: Mon, 6 Nov 2023 12:32:22 +0800 Subject: [PATCH 16/24] Add files --- .../Control/Helper/SettingsExpanderHelper.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs b/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs new file mode 100644 index 00000000..50346c26 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.WinUI.Controls; +using Microsoft.UI.Xaml; + +namespace Snap.Hutao.Control.Helper; + +[SuppressMessage("", "SH001")] +[DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))] +public sealed partial class SettingsExpanderHelper +{ + private static void OnIsItemsEnabledChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) + { + SettingsExpander expander = (SettingsExpander)dp; + foreach (SettingsCard item in expander.Items.Cast()) + { + item.IsEnabled = (bool)e.NewValue; + } + } +} From 88684bff004f9cdd2ea0d31a08319d5b80c5d395 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 6 Nov 2023 12:40:19 +0800 Subject: [PATCH 17/24] code style --- .../Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs | 8 +++++--- src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs b/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs index 50346c26..3d3b9540 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Helper/SettingsExpanderHelper.cs @@ -12,10 +12,12 @@ public sealed partial class SettingsExpanderHelper { private static void OnIsItemsEnabledChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) { - SettingsExpander expander = (SettingsExpander)dp; - foreach (SettingsCard item in expander.Items.Cast()) + foreach (object item in ((SettingsExpander)dp).Items) { - item.IsEnabled = (bool)e.NewValue; + if (item is Microsoft.UI.Xaml.Controls.Control control) + { + control.IsEnabled = (bool)e.NewValue; + } } } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index dfef36b1..8566e7f2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -200,11 +200,11 @@ + IsExpanded="True"> From b8886c5cd318114cc8b8a46d22ddc67ac8b3b8c0 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 6 Nov 2023 13:46:59 +0800 Subject: [PATCH 18/24] fix #1072 --- .../Snap.Hutao/View/Page/SettingPage.xaml | 119 +++++++++--------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 6c867801..269841f3 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -94,63 +94,68 @@ - - - [HighQuality] -internal class PathMd5 +internal partial class PathMd5 { /// /// 下载地址 @@ -25,4 +27,10 @@ internal class PathMd5 /// 显示名称 /// public string DisplayName { get => System.IO.Path.GetFileName(Path); } + + [Command("CopyPathCommand")] + private void CopyPathToClipboard() + { + Ioc.Default.GetRequiredService().SetText(Path); + } } From 5bc957c6a565c59e680f837f5311c8aa154bca0b Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 6 Nov 2023 20:32:29 +0800 Subject: [PATCH 23/24] fix spiral abyss crash when using 4.2 metadata --- .../Model/Metadata/MonsterRelationship.cs | 2 ++ .../Snap.Hutao/ViewModel/SpiralAbyss/BattleWave.cs | 11 ++++++++++- .../Snap.Hutao/ViewModel/SpiralAbyss/MonsterView.cs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/MonsterRelationship.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/MonsterRelationship.cs index e8f157d9..2493b104 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/MonsterRelationship.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/MonsterRelationship.cs @@ -19,6 +19,8 @@ internal static class MonsterRelationship 5071U => 507U, // 幻形花鼠 · 水 (强化) 5102U => 510U, // 历经百战的浊水粉碎幻灵 5112U => 511U, // 历经百战的浊水喷吐幻灵 + 30605U => 30603U, // 历经百战的霜剑律从 + 30606U => 30604U, // 历经百战的幽风铃兰 60402U => 60401U, // (火)岩龙蜥 60403U => 60401U, // (冰)岩龙蜥 60404U => 60401U, // (雷)岩龙蜥 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleWave.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleWave.cs index ee121199..73ed507c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleWave.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/BattleWave.cs @@ -4,6 +4,7 @@ using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Tower; +using Snap.Hutao.Model.Primitive; namespace Snap.Hutao.ViewModel.SpiralAbyss; @@ -12,7 +13,7 @@ internal sealed class BattleWave : IMappingFrom MonsterView.From(m, context.IdMonsterMap[MonsterRelationship.Normalize(m.Id)])); + Monsters = towerWave.Monsters.SelectList(m => CreateMonsterViewOrDefault(m, context)); } public string Description { get; } @@ -23,4 +24,12 @@ internal sealed class BattleWave : IMappingFrom { + private MonsterView(MonsterRelationshipId id) + { + Name = $"Unknown {id}"; + Icon = Web.HutaoEndpoints.UIIconNone; + Count = 1; + } + private MonsterView(TowerMonster towerMonster, Model.Metadata.Monster.Monster metaMonster) { Name = metaMonster.Name; @@ -19,6 +27,11 @@ internal sealed class MonsterView : INameIcon, IMappingFrom Date: Mon, 6 Nov 2023 20:53:29 +0800 Subject: [PATCH 24/24] add copy hint for #1074 --- src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx | 3 +++ .../Snap.Hutao/View/Control/LaunchGameResourceExpander.xaml | 4 ++-- .../Web/Hoyolab/SdkStatic/Hk4e/Launcher/PathMd5.cs | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 643e811e..9a8c6ef5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2741,6 +2741,9 @@ 武器活动祈愿 + + 下载链接复制成功 + 验证失败,请手动验证或前往「米游社-我的角色」页面查看 diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/LaunchGameResourceExpander.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/LaunchGameResourceExpander.xaml index 8a355035..0ae5e77a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/LaunchGameResourceExpander.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/LaunchGameResourceExpander.xaml @@ -49,7 +49,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Command="{Binding CopyPathCommand}" - Content="{StaticResource FontIconContentOpenInNewWindow}" + Content="{StaticResource FontIconContentCopy}" FontFamily="{StaticResource SymbolThemeFontFamily}"/> @@ -93,7 +93,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Command="{Binding CopyPathCommand}" - Content="{StaticResource FontIconContentOpenInNewWindow}" + Content="{StaticResource FontIconContentCopy}" FontFamily="{StaticResource SymbolThemeFontFamily}"/> ().SetText(Path); + IServiceProvider serviceProvider = Ioc.Default; + serviceProvider.GetRequiredService().SetText(Path); + serviceProvider.GetRequiredService().Success(SH.WebGameResourcePathCopySucceed); } }