diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index 5378799a..4f049ed9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -15,9 +15,6 @@ namespace Snap.Hutao.Core; /// internal static class CoreEnvironment { - private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\"; - private const string MachineGuidValue = "MachineGuid"; - // 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd /// @@ -71,6 +68,9 @@ internal static class CoreEnvironment WriteIndented = true, }; + private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\"; + private const string MachineGuidValue = "MachineGuid"; + static CoreEnvironment() { Version = Package.Current.Id.Version.ToVersion(); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs index bc3add7e..ad22f519 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs @@ -26,7 +26,6 @@ internal class DbCurrent /// /// 数据集 /// 消息器 - /// public DbCurrent(DbSet dbSet, IMessenger messenger) { this.dbSet = dbSet; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs index e0e87e8f..019fb9e1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Snap.Hutao.Model.Entity; namespace Snap.Hutao.Core.Database; @@ -91,4 +92,4 @@ public static class DbSetExtension dbSet.Update(entity); return dbSet.Context().SaveChanges(); } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/SettingEntryHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/SettingEntryHelper.cs new file mode 100644 index 00000000..f336d7e6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/SettingEntryHelper.cs @@ -0,0 +1,95 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Model.Entity; + +namespace Snap.Hutao.Core.Database; + +/// +/// 设置帮助类 +/// +public static class SettingEntryHelper +{ + /// + /// 获取或添加一个对应的设置 + /// + /// 设置集 + /// 键 + /// 值 + /// 设置 + public static SettingEntry SingleOrAdd(this DbSet dbSet, string key, string value) + { + SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key); + + if (entry == null) + { + entry = new(key, value); + dbSet.Add(entry); + dbSet.Context().SaveChanges(); + } + + return entry; + } + + /// + /// 获取或添加一个对应的设置 + /// + /// 设置集 + /// 键 + /// 值工厂 + /// 设置 + public static SettingEntry SingleOrAdd(this DbSet dbSet, string key, Func valueFactory) + { + SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key); + + if (entry == null) + { + entry = new(key, valueFactory()); + dbSet.Add(entry); + dbSet.Context().SaveChanges(); + } + + return entry; + } + + /// + /// 获取 Boolean 值 + /// + /// 设置 + /// + public static bool GetBoolean(this SettingEntry entry) + { + return bool.Parse(entry.Value!); + } + + /// + /// 设置 Boolean 值 + /// + /// 设置 + /// 值 + public static void SetBoolean(this SettingEntry entry, bool value) + { + entry.Value = value.ToString(); + } + + /// + /// 获取 Int32 值 + /// + /// 设置 + /// + public static int GetInt32(this SettingEntry entry) + { + return int.Parse(entry.Value!); + } + + /// + /// 设置 Int32 值 + /// + /// 设置 + /// 值 + public static void SetInt32(this SettingEntry entry, int value) + { + entry.Value = value.ToString(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs index 26b6a82d..bcbb8dc5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs @@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.IO.Ini; internal static class IniSerializer { /// - /// 异步反序列化 + /// 反序列化 /// /// 文件流 /// Ini 元素集合 @@ -44,4 +44,20 @@ internal static class IniSerializer } } } + + /// + /// 序列化 + /// + /// 写入的流 + /// 元素 + public static void Serialize(FileStream fileStream, IEnumerable elements) + { + using (TextWriter writer = new StreamWriter(fileStream)) + { + foreach (IniElement element in elements) + { + writer.WriteLine(element.ToString()); + } + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index 1eda1194..5672c517 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -5,6 +5,7 @@ using Microsoft.Windows.AppLifecycle; using Snap.Hutao.Core.Threading; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Navigation; +using System.Security.Principal; namespace Snap.Hutao.Core.LifeCycle; @@ -20,6 +21,19 @@ internal static class Activation private static readonly SemaphoreSlim ActivateSemaphore = new(1); + /// + /// 获取是否提升了权限 + /// + /// 是否提升了权限 + public static bool GetElevated() + { + using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) + { + WindowsPrincipal principal = new(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + /// /// 响应激活事件 /// 激活事件一般不会在UI线程上触发 @@ -70,6 +84,18 @@ internal static class Activation case LaunchGame: { + await ThreadHelper.SwitchToMainThreadAsync(); + if (!MainWindow.IsPresent) + { + _ = Ioc.Default.GetRequiredService(); + } + else + { + await Ioc.Default + .GetRequiredService() + .NavigateAsync(INavigationAwaiter.Default, true).ConfigureAwait(false); + } + break; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs index e41aea32..497d1018 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs @@ -18,16 +18,18 @@ namespace Snap.Hutao.Core.Windowing; /// 窗口管理器 /// 主要包含了针对窗体的 P/Inoke 逻辑 /// -internal sealed class ExtendedWindow +/// 窗体类型 +internal sealed class ExtendedWindow + where TWindow : Window, IExtendedWindowSource { private readonly HWND handle; private readonly AppWindow appWindow; - private readonly Window window; + private readonly TWindow window; private readonly FrameworkElement titleBar; - private readonly ILogger logger; - private readonly WindowSubclassManager subclassManager; + private readonly ILogger> logger; + private readonly WindowSubclassManager subclassManager; private readonly bool useLegacyDragBar; @@ -36,11 +38,11 @@ internal sealed class ExtendedWindow /// /// 窗口 /// 充当标题栏的元素 - private ExtendedWindow(Window window, FrameworkElement titleBar) + private ExtendedWindow(TWindow window, FrameworkElement titleBar) { this.window = window; this.titleBar = titleBar; - logger = Ioc.Default.GetRequiredService>(); + logger = Ioc.Default.GetRequiredService>>(); handle = (HWND)WindowNative.GetWindowHandle(window); @@ -48,7 +50,7 @@ internal sealed class ExtendedWindow appWindow = AppWindow.GetFromWindowId(windowId); useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported(); - subclassManager = new(handle, useLegacyDragBar); + subclassManager = new(window, handle, useLegacyDragBar); InitializeWindow(); } @@ -57,11 +59,10 @@ internal sealed class ExtendedWindow /// 初始化 /// /// 窗口 - /// 标题栏 /// 实例 - public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar) + public static ExtendedWindow Initialize(TWindow window) { - return new(window, titleBar); + return new(window, window.TitleBar); } private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar) @@ -103,7 +104,8 @@ internal sealed class ExtendedWindow appWindow.Title = "胡桃"; ExtendsContentIntoTitleBar(); - Persistence.RecoverOrInit(appWindow); + + Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize); // Log basic window state here. (string pos, string size) = GetPostionAndSize(appWindow); @@ -115,14 +117,18 @@ internal sealed class ExtendedWindow logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed"); bool subClassApplied = subclassManager.TrySetWindowSubclass(); - logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed"); + logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed"); window.Closed += OnWindowClosed; } private void OnWindowClosed(object sender, WindowEventArgs args) { - Persistence.Save(appWindow); + if (window.PersistSize) + { + Persistence.Save(appWindow); + } + subclassManager?.Dispose(); } @@ -155,4 +161,4 @@ internal sealed class ExtendedWindow RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale); appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray()); } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/IExtendedWindowSource.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/IExtendedWindowSource.cs new file mode 100644 index 00000000..2c4e6e6d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/IExtendedWindowSource.cs @@ -0,0 +1,37 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Windows.Graphics; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Snap.Hutao.Core.Windowing; + +/// +/// 为扩展窗体提供必要的选项 +/// +/// 窗体类型 +internal interface IExtendedWindowSource +{ + /// + /// 提供的标题栏 + /// + FrameworkElement TitleBar { get; } + + /// + /// 是否持久化尺寸 + /// + bool PersistSize { get; } + + /// + /// 初始大小 + /// + SizeInt32 InitSize { get; } + + /// + /// 处理最大最小信息 + /// + /// 信息指针 + /// 缩放比 + unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs index 431f6964..2d9ce21f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/Persistence.cs @@ -21,21 +21,26 @@ internal static class Persistence /// 设置窗体位置 /// /// 应用窗体 - public static void RecoverOrInit(AppWindow appWindow) + /// 持久化尺寸 + /// 初始尺寸 + public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size) { // Set first launch size. HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id); - SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd); - RectInt32 rect = StructMarshal.RectInt32(size); + SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd); + RectInt32 rect = StructMarshal.RectInt32(transformedSize); - RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect); - if (target.Width * target.Height < 848 * 524) + if (persistSize) { - target = rect; + RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect); + if (persistedSize.Width * persistedSize.Height > 848 * 524) + { + rect = persistedSize; + } } - TransformToCenterScreen(ref target); - appWindow.MoveAndResize(target); + TransformToCenterScreen(ref rect); + appWindow.MoveAndResize(rect); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs index ae87de4e..c4c91c02 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.UI.Xaml; using Windows.Win32.Foundation; using Windows.Win32.UI.Shell; using Windows.Win32.UI.WindowsAndMessaging; @@ -11,14 +12,14 @@ namespace Snap.Hutao.Core.Windowing; /// /// 窗体子类管理器 /// -internal class WindowSubclassManager : IDisposable +/// 窗体类型 +internal class WindowSubclassManager : IDisposable + where TWindow : Window, IExtendedWindowSource { private const int WindowSubclassId = 101; private const int DragBarSubclassId = 102; - private const int MinWidth = 848; - private const int MinHeight = 524; - + private readonly TWindow window; private readonly HWND hwnd; private readonly bool isLegacyDragBar; private HWND hwndDragBar; @@ -30,12 +31,13 @@ internal class WindowSubclassManager : IDisposable /// /// 构造一个新的窗体子类管理器 /// + /// 窗体实例 /// 窗体句柄 /// 是否为经典标题栏区域 - public WindowSubclassManager(HWND hwnd, bool isLegacyDragBar) + public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar) { - Must.NotNull(hwnd); - this.hwnd = hwnd; + this.window = window; + this.hwnd = Must.NotNull(hwnd); this.isLegacyDragBar = isLegacyDragBar; } @@ -85,9 +87,7 @@ internal class WindowSubclassManager : IDisposable case WM_GETMINMAXINFO: { double scalingFactor = Persistence.GetScaleForWindow(hwnd); - MINMAXINFO* info = (MINMAXINFO*)lParam.Value; - info->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info->ptMinTrackSize.X); - info->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info->ptMinTrackSize.Y); + window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor); break; } } diff --git a/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml b/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml index 56cbbfea..8d607b0e 100644 --- a/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml +++ b/src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml @@ -1,12 +1,63 @@  - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Injection(InjectAs.Singleton)] [SuppressMessage("", "CA1001")] -public sealed partial class MainWindow : Window +public sealed partial class MainWindow : Window, IExtendedWindowSource { + private const int MinWidth = 848; + private const int MinHeight = 524; + /// /// 构造一个新的主窗体 /// public MainWindow() { InitializeComponent(); - ExtendedWindow.Initialize(this, TitleBarView.DragArea); + ExtendedWindow.Initialize(this); + IsPresent = true; + Closed += (s, e) => IsPresent = false; + } + + /// + /// 是否打开 + /// + public static bool IsPresent { get; private set; } + + /// + public FrameworkElement TitleBar { get => TitleBarView.DragArea; } + + /// + public bool PersistSize { get => true; } + + /// + public SizeInt32 InitSize { get => new(1200, 741); } + + /// + public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor) + { + pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X); + pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs new file mode 100644 index 00000000..1f33b341 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs @@ -0,0 +1,215 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Context.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20221031104940_GameAccount")] + partial class GameAccount + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + 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("Info") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("avatar_infos"); + }); + + 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.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cookie") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + 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.GachaItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.cs new file mode 100644 index 00000000..dff7e721 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.cs @@ -0,0 +1,34 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + public partial class GameAccount : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "game_accounts", + columns: table => new + { + InnerId = table.Column(type: "TEXT", nullable: false), + AttachUid = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + MihoyoSDK = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_game_accounts", x => x.InnerId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "game_accounts"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 1a7dfe2a..a20c886d 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", "6.0.9"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => { @@ -131,6 +131,31 @@ namespace Snap.Hutao.Migrations 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.SettingEntry", b => { b.Property("Key") diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs index 1df4de43..d9423096 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs @@ -56,7 +56,7 @@ public class User : ObservableObject } /// - public Cookie Cookie + public Cookie? Cookie { get => inner.Cookie; set @@ -71,7 +71,7 @@ public class User : ObservableObject /// public bool HasSToken { - get => inner.Cookie.ContainsSToken(); + get => inner.Cookie!.ContainsSToken(); } /// @@ -84,17 +84,6 @@ public class User : ObservableObject /// public bool IsInitialized { get => isInitialized; } - /// - /// 更新SToken - /// - /// uid - /// cookie - internal void UpdateSToken(string uid, Cookie cookie) - { - Cookie.InsertSToken(uid, cookie); - OnPropertyChanged(nameof(HasSToken)); - } - /// /// 从数据库恢复用户 /// @@ -125,6 +114,17 @@ public class User : ObservableObject return successful ? user : null; } + /// + /// 更新SToken + /// + /// uid + /// cookie + internal void UpdateSToken(string uid, Cookie cookie) + { + Cookie!.InsertSToken(uid, cookie); + OnPropertyChanged(nameof(HasSToken)); + } + private async Task InitializeCoreAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default) { if (isInitialized) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs index 97fe2ed7..b587a07b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs @@ -1,9 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Binding.LaunchGame; -using Snap.Hutao.Web.Hoyolab; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -13,8 +11,11 @@ namespace Snap.Hutao.Model.Entity; /// 游戏内账号 /// [Table("game_accounts")] -public class GameAccount : ISelectable +public class GameAccount : INotifyPropertyChanged { + /// + public event PropertyChangedEventHandler? PropertyChanged; + /// /// 内部Id /// @@ -22,9 +23,6 @@ public class GameAccount : ISelectable [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid InnerId { get; set; } - /// - public bool IsSelected { get; set; } - /// /// 对应的Uid /// @@ -41,7 +39,43 @@ public class GameAccount : ISelectable public string Name { get; set; } = default!; /// - /// MIHOYOSDK_ADL_PROD_CN_h3123967166 + /// [MIHOYOSDK_ADL_PROD_CN_h3123967166] + /// see /// public string MihoyoSDK { get; set; } = default!; + + /// + /// 构造一个新的游戏内账号 + /// + /// 名称 + /// sdk + /// 游戏内账号 + public static GameAccount Create(string name, string sdk) + { + return new() + { + Name = name, + MihoyoSDK = sdk, + }; + } + + /// + /// 更新绑定的Uid + /// + /// uid + public void UpdateAttachUid(string? uid) + { + AttachUid = uid; + PropertyChanged?.Invoke(this, new(nameof(AttachUid))); + } + + /// + /// 更新名称 + /// + /// 新名称 + public void UpdateName(string name) + { + Name = name; + PropertyChanged?.Invoke(this, new(nameof(Name))); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs index 74d06f99..8a947f70 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.cs @@ -22,6 +22,36 @@ public class SettingEntry /// public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible"; + /// + /// 启动游戏 全屏 + /// + public const string LaunchIsFullScreen = "Launch.IsFullScreen"; + + /// + /// 启动游戏 无边框 + /// + public const string LaunchIsBorderless = "Launch.IsBorderless"; + + /// + /// 启动游戏 宽度 + /// + public const string LaunchScreenWidth = "Launch.ScreenWidth"; + + /// + /// 启动游戏 高度 + /// + public const string LaunchScreenHeight = "Launch.ScreenHeight"; + + /// + /// 启动游戏 解锁帧率 + /// + public const string LaunchUnlockFps = "Launch.UnlockFps"; + + /// + /// 启动游戏 目标帧率 + /// + public const string LaunchTargetFps = "Launch.TargetFps"; + /// /// 构造一个新的设置入口 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index 24cc0955..1712bfda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -29,7 +29,7 @@ public class User : ISelectable /// /// 用户的Cookie /// - public Cookie Cookie { get; set; } = default!; + public Cookie? Cookie { get; set; } /// /// 创建一个新的用户 diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index dae27fae..ce720832 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -2,19 +2,22 @@ + IgnorableNamespaces="uap desktop6 rescap"> + Version="1.1.18.0" /> 胡桃 DGP Studio Assets\StoreLogo.png + + disabled @@ -50,6 +53,7 @@ - + + diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs index 441cf087..2f281056 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppCenter/Model/Log/LogHelper.cs @@ -49,7 +49,6 @@ public static class LogHelper if (frames.Length > 0 && frames[0].HasNativeImage()) { - } return current; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs index 58c5dafc..27ee4f64 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlWebCacheProvider.cs @@ -54,59 +54,34 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider { using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) { - using (BinaryReader reader = new(fileStream)) + using (MemoryStream memoryStream = new()) { - string url = string.Empty; - while (!reader.EndOfStream()) - { - uint test = reader.ReadUInt32(); - - if (test == 0x2F302F31) - { - byte[] chars = ReadBytesUntilZero(reader); - string result = Encoding.UTF8.GetString(chars.AsSpan()); - - if (result.Contains("&auth_appid=webview_gacha")) - { - url = result; - } - - // align up - long offset = reader.BaseStream.Position % 128; - reader.BaseStream.Position += 128 - offset; - } - } - - return new(!string.IsNullOrEmpty(url), url); + await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); + string? result = Match(memoryStream); + return new(!string.IsNullOrEmpty(result), result!); } } } } else { - return new(false, $"未正确提供原神路径,或当前设置的路径不正确"); + return new(false, "未正确提供原神路径,或当前设置的路径不正确"); } } - private static byte[] ReadBytesUntilZero(BinaryReader binaryReader) + private static string? Match(MemoryStream stream) { - return ReadByteEnumerableUntilZero(binaryReader).ToArray(); - } + ReadOnlySpan span = stream.ToArray(); + ReadOnlySpan match = Encoding.UTF8.GetBytes("https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html"); + ReadOnlySpan zero = Encoding.UTF8.GetBytes("\0"); - private static IEnumerable ReadByteEnumerableUntilZero(BinaryReader binaryReader) - { - while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length) + int index = span.LastIndexOf(match); + if (index >= 0) { - byte b = binaryReader.ReadByte(); - - if (b == 0x00) - { - yield break; - } - else - { - yield return b; - } + int length = span[index..].IndexOf(zero); + return Encoding.UTF8.GetString(span.Slice(index, length)); } + + return null; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameAccountRegistryInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameAccountRegistryInterop.cs new file mode 100644 index 00000000..5759a38b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameAccountRegistryInterop.cs @@ -0,0 +1,49 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Win32; +using Snap.Hutao.Model.Entity; +using System.Text; + +namespace Snap.Hutao.Service.Game; + +/// +/// 定义了对注册表的操作 +/// +internal static class GameAccountRegistryInterop +{ + private const string GenshinKey = @"HKEY_CURRENT_USER\Software\miHoYo\原神"; + private const string SdkKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166"; + + /// + /// 设置键值 + /// + /// 账户 + /// 账号是否设置 + public static bool Set(GameAccount? account) + { + if (account != null) + { + Registry.SetValue(GenshinKey, SdkKey, Encoding.UTF8.GetBytes(account.MihoyoSDK)); + return true; + } + + return false; + } + + /// + /// 在注册表中获取账号信息 + /// + /// 当前注册表中的信息 + public static string? Get() + { + object? sdk = Registry.GetValue(GenshinKey, SdkKey, Array.Empty()); + + if (sdk is byte[] bytes) + { + return Encoding.UTF8.GetString(bytes); + } + + return null; + } +} \ 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 index 722e2eb2..75898956 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -8,9 +8,11 @@ using Snap.Hutao.Core; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.IO.Ini; using Snap.Hutao.Core.Threading; +using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game.Locator; using Snap.Hutao.Service.Game.Unlocker; +using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; @@ -24,6 +26,7 @@ namespace Snap.Hutao.Service.Game; internal class GameService : IGameService { private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}"; + private const string ConfigFile = "config.ini"; private readonly IServiceScopeFactory scopeFactory; private readonly IMemoryCache memoryCache; @@ -136,7 +139,7 @@ internal class GameService : IGameService public MultiChannel GetMultiChannel() { string gamePath = GetGamePathSkipLocator(); - string configPath = Path.Combine(gamePath, "config.ini"); + string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile); using (FileStream stream = File.OpenRead(configPath)) { @@ -148,11 +151,61 @@ internal class GameService : IGameService } } - public async Task> GetGameAccountCollectionAsync() + /// + public void SetMultiChannel(LaunchScheme scheme) + { + string gamePath = GetGamePathSkipLocator(); + string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile); + + List elements; + using (FileStream readStream = File.OpenRead(configPath)) + { + elements = IniSerializer.Deserialize(readStream).ToList(); + } + + foreach (IniElement element in elements) + { + if (element is IniParameter parameter) + { + if (parameter.Key == "channel") + { + parameter.Value = scheme.Channel; + } + + if (parameter.Key == "sub_channel") + { + parameter.Value = scheme.SubChannel; + } + } + } + + using (FileStream writeStream = File.Create(configPath)) + { + IniSerializer.Serialize(writeStream, elements); + } + } + + /// + public bool IsGameRunning() + { + if (gameSemaphore.CurrentCount == 0) + { + return true; + } + + return Process.GetProcessesByName("YuanShen.exe").Any(); + } + + /// + public ObservableCollection GetGameAccountCollection() { if (gameAccounts == null) { - + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + gameAccounts = new(appDbContext.GameAccounts.ToList()); + } } return gameAccounts; @@ -161,19 +214,19 @@ internal class GameService : IGameService /// public async ValueTask LaunchAsync(LaunchConfiguration configuration) { - if (gameSemaphore.CurrentCount == 0) + if (IsGameRunning()) { return; } string gamePath = GetGamePathSkipLocator(); + // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html string commandLine = new CommandLineBuilder() .AppendIf("-popupwindow", configuration.IsBorderless) .Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0) .Append("-screen-width", configuration.ScreenWidth) .Append("-screen-height", configuration.ScreenHeight) - .Append("-monitor", configuration.Monitor) .Build(); Process game = new() @@ -190,22 +243,122 @@ internal class GameService : IGameService using (await gameSemaphore.EnterAsync().ConfigureAwait(false)) { - if (configuration.UnlockFPS) + try { - IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS); - - TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); - TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); - TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000); - await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false); - } - else - { - if (game.Start()) + if (configuration.UnlockFPS) { - await game.WaitForExitAsync().ConfigureAwait(false); + IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS); + + TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); + TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); + TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000); + if (game.Start()) + { + await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false); + } + } + else + { + if (game.Start()) + { + await game.WaitForExitAsync().ConfigureAwait(false); + } + } + } + catch (Win32Exception) + { + } + } + } + + /// + public async ValueTask DetectGameAccountAsync() + { + Must.NotNull(gameAccounts!); + + string? registrySdk = GameAccountRegistryInterop.Get(); + if (!string.IsNullOrEmpty(registrySdk)) + { + GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); + + if (account == null) + { + MainWindow mainWindow = Ioc.Default.GetRequiredService(); + (bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false); + + if (isOk) + { + account = GameAccount.Create(name, registrySdk); + + // sync cache + await ThreadHelper.SwitchToMainThreadAsync(); + gameAccounts.Add(GameAccount.Create(name, registrySdk)); + + // sync database + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.GameAccounts.AddAndSave(account); + } } } } } + + /// + public bool SetGameAccount(GameAccount account) + { + return GameAccountRegistryInterop.Set(account); + } + + /// + public void AttachGameAccountToUid(GameAccount gameAccount, string uid) + { + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + IQueryable oldAccounts = appDbContext.GameAccounts.Where(a => a.AttachUid == uid); + + foreach (GameAccount account in oldAccounts) + { + account.UpdateAttachUid(null); + appDbContext.GameAccounts.UpdateAndSave(account); + } + + gameAccount.UpdateAttachUid(uid); + appDbContext.GameAccounts.UpdateAndSave(gameAccount); + } + } + + /// + public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) + { + MainWindow mainWindow = Ioc.Default.GetRequiredService(); + (bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false); + + if (isOk) + { + gameAccount.UpdateName(name); + + // sync database + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.GameAccounts.UpdateAndSave(gameAccount); + } + } + } + + /// + public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) + { + Must.NotNull(gameAccounts!).Remove(gameAccount); + + await Task.Yield(); + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.GameAccounts.RemoveAndSave(gameAccount); + } + } } \ 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/IGameService.cs index 13020091..7f325ed5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. using Snap.Hutao.Core.Threading; +using Snap.Hutao.Model.Binding.LaunchGame; +using Snap.Hutao.Model.Entity; +using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Game; @@ -10,6 +13,26 @@ namespace Snap.Hutao.Service.Game; /// internal interface IGameService { + /// + /// 将账号绑定到对应的Uid + /// 清除老账号的绑定状态 + /// + /// 游戏内账号 + /// uid + void AttachGameAccountToUid(GameAccount gameAccount, string uid); + + /// + /// 检测并尝试添加游戏内账户 + /// + /// 任务 + ValueTask DetectGameAccountAsync(); + + /// + /// 获取游戏内账号集合 + /// + /// 游戏内账号集合 + ObservableCollection GetGameAccountCollection(); + /// /// 异步获取游戏路径 /// @@ -28,6 +51,12 @@ internal interface IGameService /// 多通道值 MultiChannel GetMultiChannel(); + /// + /// 游戏是否正在运行 + /// + /// 是否正在运行 + bool IsGameRunning(); + /// /// 异步启动 /// @@ -35,9 +64,36 @@ internal interface IGameService /// 任务 ValueTask LaunchAsync(LaunchConfiguration configuration); + /// + /// 异步修改游戏账号名称 + /// + /// 游戏账号 + /// 任务 + ValueTask ModifyGameAccountAsync(GameAccount gameAccount); + /// /// 重写游戏路径 /// /// 路径 void OverwriteGamePath(string path); + + /// + /// 异步尝试移除账号 + /// + /// 账号 + /// 任务 + ValueTask RemoveGameAccountAsync(GameAccount gameAccount); + + /// + /// 修改注册表中的账号信息 + /// + /// 账号 + /// 是否设置成功 + bool SetGameAccount(GameAccount account); + + /// + /// 设置多通道值 + /// + /// 方案 + void SetMultiChannel(LaunchScheme scheme); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs index 9c62a359..0bf32659 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs @@ -6,40 +6,55 @@ namespace Snap.Hutao.Service.Game; /// /// 启动游戏配置 /// -internal struct LaunchConfiguration +internal readonly struct LaunchConfiguration { /// /// 是否全屏,全屏时无边框设置将被覆盖 /// - public bool IsFullScreen { get; set; } + public readonly bool IsFullScreen; /// /// 是否为无边框窗口 /// - public bool IsBorderless { get; private set; } - - /// - /// 是否启用解锁帧率 - /// - public bool UnlockFPS { get; private set; } - - /// - /// 目标帧率 - /// - public int TargetFPS { get; private set; } + public readonly bool IsBorderless; /// /// 窗口宽度 /// - public int ScreenWidth { get; private set; } + public readonly int ScreenWidth; /// /// 窗口高度 /// - public int ScreenHeight { get; private set; } + public readonly int ScreenHeight; /// - /// 显示器编号 + /// 是否启用解锁帧率 /// - public int Monitor { get; private set; } + public readonly bool UnlockFPS; + + /// + /// 目标帧率 + /// + public readonly int TargetFPS; + + /// + /// 构造一个新的启动配置 + /// + /// 全屏 + /// 无边框 + /// 宽度 + /// 高度 + /// 解锁帧率 + /// 目标帧率 + public LaunchConfiguration(bool isFullScreen, bool isBorderless, int screenWidth, int screenHeight, bool unlockFps, int targetFps) + { + IsFullScreen = isFullScreen; + IsBorderless = isBorderless; + ScreenHeight = screenHeight; + ScreenWidth = screenWidth; + ScreenHeight = screenHeight; + UnlockFPS = unlockFps; + TargetFPS = targetFps; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs index 576a43c5..864cfecb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs @@ -181,13 +181,16 @@ internal class NavigationService : INavigationService /// public void GoBack() { - bool canGoBack = Frame?.CanGoBack ?? false; - - if (canGoBack) + Program.DispatcherQueue!.TryEnqueue(() => { - Frame!.GoBack(); - SyncSelectedNavigationViewItemWith(Frame.Content.GetType()); - } + bool canGoBack = Frame?.CanGoBack ?? false; + + if (canGoBack) + { + Frame!.GoBack(); + SyncSelectedNavigationViewItemWith(Frame.Content.GetType()); + } + }); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index ac245f89..c6b52f4f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -95,7 +95,6 @@ internal class UserService : IUserService using (IServiceScope scope = scopeFactory.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - appDbContext.Users.RemoveAndSave(user.Entity); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 05b1ee58..55ae9b44 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -60,7 +60,7 @@ - + @@ -150,6 +150,11 @@ + + + MSBuild:Compile + + MSBuild:Compile @@ -190,11 +195,6 @@ MSBuild:Compile - - - MSBuild:Compile - - MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GameAccountNameDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GameAccountNameDialog.xaml new file mode 100644 index 00000000..206bb031 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GameAccountNameDialog.xaml @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GameAccountNameDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GameAccountNameDialog.xaml.cs new file mode 100644 index 00000000..81c103a7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GameAccountNameDialog.xaml.cs @@ -0,0 +1,35 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core.Threading; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 游戏账号命名对话框 +/// +public sealed partial class GameAccountNameDialog : ContentDialog +{ + /// + /// 构造一个新的游戏账号命名对话框 + /// + /// 窗体 + public GameAccountNameDialog(Window window) + { + InitializeComponent(); + XamlRoot = window.Content.XamlRoot; + } + + /// + /// 获取输入的Cookie + /// + /// 输入的结果 + public async Task> GetInputNameAsync() + { + ContentDialogResult result = await ShowAsync(); + string text = InputText.Text; + return new(result == ContentDialogResult.Primary && (!string.IsNullOrEmpty(text)), text); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml deleted file mode 100644 index d27860cc..00000000 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml +++ /dev/null @@ -1,33 +0,0 @@ - - - 1600 - 200 - 1200 - - - - - - - - - - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs deleted file mode 100644 index 17f6434c..00000000 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.Web.WebView2.Core; -using Snap.Hutao.Core.Threading; -using Snap.Hutao.Web.Hoyolab; - -namespace Snap.Hutao.View.Dialog; - -/// -/// 用户自动Cookie对话框 -/// -public sealed partial class UserAutoCookieDialog : ContentDialog -{ - private Cookie? cookie; - - /// - /// 构造一个新的用户自动Cookie对话框 - /// - /// 依赖窗口 - public UserAutoCookieDialog(Window window) - { - InitializeComponent(); - XamlRoot = window.Content.XamlRoot; - } - - /// - /// 获取输入的Cookie - /// - /// 输入的结果 - public async Task> GetInputCookieAsync() - { - ContentDialogResult result = await ShowAsync(); - return new(result == ContentDialogResult.Primary && cookie != null, cookie!); - } - - [SuppressMessage("", "VSTHRD100")] - private async void OnRootLoaded(object sender, RoutedEventArgs e) - { - await WebView.EnsureCoreWebView2Async(); - WebView.CoreWebView2.SourceChanged += OnCoreWebView2SourceChanged; - - CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager; - IReadOnlyList cookies = await manager.GetCookiesAsync("https://user.mihoyo.com"); - foreach (CoreWebView2Cookie item in cookies) - { - manager.DeleteCookie(item); - } - - WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password"); - } - - [SuppressMessage("", "VSTHRD100")] - private async void OnCoreWebView2SourceChanged(CoreWebView2 sender, CoreWebView2SourceChangedEventArgs args) - { - if (sender != null) - { - if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home") - { - CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager; - IReadOnlyList cookies = await manager.GetCookiesAsync("https://user.mihoyo.com"); - cookie = Cookie.FromCoreWebView2Cookies(cookies); - WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged; - } - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml index 1f751da7..237ff4d5 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserDialog.xaml @@ -7,7 +7,7 @@ xmlns:settings="using:SettingsUI.Controls" mc:Ignorable="d" IsPrimaryButtonEnabled="False" - Title="设置米游社Cookie" + Title="设置 Cookie" DefaultButton="Primary" PrimaryButtonText="请输入Cookie" CloseButtonText="取消" @@ -20,17 +20,19 @@ TextChanged="InputTextChanged" PlaceholderText="在此处输入" VerticalAlignment="Top"/> + + + NavigateUri="https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/> diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index de97920b..4d654179 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -1,19 +1,32 @@ - + + + + + + + @@ -34,10 +47,14 @@ Header="服务器" Description="切换游戏服务器,B服用户需要自备额外的 PCGameSDK.dll 文件"> - + - + @@ -45,7 +62,6 @@ - - + /// 游戏服务 + /// 数据库上下文 /// 异步命令工厂 - public LaunchGameViewModel(IGameService gameService, IAsyncRelayCommandFactory asyncRelayCommandFactory) + public LaunchGameViewModel(IGameService gameService, AppDbContext appDbContext, IAsyncRelayCommandFactory asyncRelayCommandFactory) { this.gameService = gameService; + this.appDbContext = appDbContext; OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync); LaunchCommand = asyncRelayCommandFactory.Create(LaunchAsync); DetectGameAccountCommand = asyncRelayCommandFactory.Create(DetectGameAccountAsync); - ModifyGameAccountCommand = asyncRelayCommandFactory.Create(ModifyGameAccountAsync); - RemoveGameAccountCommand = asyncRelayCommandFactory.Create(RemoveGameAccountAsync); + ModifyGameAccountCommand = asyncRelayCommandFactory.Create(ModifyGameAccountAsync); + RemoveGameAccountCommand = asyncRelayCommandFactory.Create(RemoveGameAccountAsync); + AttachGameAccountCommand = new RelayCommand(AttachGameAccountToCurrentUserGameRole); } /// @@ -106,6 +121,12 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation /// public int TargetFps { get => targetFps; set => SetProperty(ref targetFps, value); } + /// + /// 是否提权 + /// + [SuppressMessage("Performance", "CA1822")] + public bool IsElevated { get => Activation.GetElevated(); } + /// /// 打开界面命令 /// @@ -131,6 +152,11 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation /// public ICommand RemoveGameAccountCommand { get; } + /// + /// 绑定到Uid命令 + /// + public ICommand AttachGameAccountCommand { get; } + private async Task OpenUIAsync() { (bool isOk, string gamePath) = await gameService.GetGamePathAsync().ConfigureAwait(false); @@ -139,26 +165,110 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation { MultiChannel multi = gameService.GetMultiChannel(); SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel); + GameAccounts = gameService.GetGameAccountCollection(); + + // Sync from Settings + RetiveSetting(); } } + private void RetiveSetting() + { + DbSet settings = appDbContext.Settings; + + isFullScreen = settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).GetBoolean(); + OnPropertyChanged(nameof(IsFullScreen)); + + isBorderless = settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).GetBoolean(); + OnPropertyChanged(nameof(IsBorderless)); + + screenWidth = settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").GetInt32(); + OnPropertyChanged(nameof(ScreenWidth)); + + screenHeight = settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").GetInt32(); + OnPropertyChanged(nameof(ScreenHeight)); + + unlockFps = settings.SingleOrAdd(SettingEntry.LaunchUnlockFps, FalseString).GetBoolean(); + OnPropertyChanged(nameof(UnlockFps)); + + targetFps = settings.SingleOrAdd(SettingEntry.LaunchTargetFps, "60").GetInt32(); + OnPropertyChanged(nameof(TargetFps)); + } + + private void SaveSetting() + { + DbSet settings = appDbContext.Settings; + settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).SetBoolean(IsFullScreen); + settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).SetBoolean(IsBorderless); + settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").SetInt32(ScreenWidth); + settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").SetInt32(ScreenHeight); + settings.SingleOrAdd(SettingEntry.LaunchUnlockFps, FalseString).SetBoolean(UnlockFps); + settings.SingleOrAdd(SettingEntry.LaunchTargetFps, "60").SetInt32(TargetFps); + appDbContext.SaveChanges(); + } + private async Task LaunchAsync() { + if (gameService.IsGameRunning()) + { + return; + } + if (SelectedScheme != null) + { + gameService.SetMultiChannel(SelectedScheme); + } + + if (SelectedGameAccount != null) + { + if (!gameService.SetGameAccount(SelectedGameAccount)) + { + Ioc.Default.GetRequiredService().Warning("切换账号失败"); + } + } + + SaveSetting(); + + LaunchConfiguration configuration = new(IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps); + await gameService.LaunchAsync(configuration).ConfigureAwait(false); } private async Task DetectGameAccountAsync() { - + await gameService.DetectGameAccountAsync().ConfigureAwait(false); } - private async Task ModifyGameAccountAsync() + private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount) { + if (gameAccount != null) + { + IUserService userService = Ioc.Default.GetRequiredService(); + IInfoBarService infoBarService = Ioc.Default.GetRequiredService(); + if (userService.Current?.SelectedUserGameRole is UserGameRole role) + { + gameService.AttachGameAccountToUid(gameAccount, role.GameUid); + } + else + { + infoBarService.Warning("当前未选择角色"); + } + } } - private async Task RemoveGameAccountAsync() + private async Task ModifyGameAccountAsync(GameAccount? gameAccount) { + if (gameAccount != null) + { + await gameService.ModifyGameAccountAsync(gameAccount).ConfigureAwait(false); + } + } + private async Task RemoveGameAccountAsync(GameAccount? gameAccount) + { + if (gameAccount != null) + { + await gameService.RemoveGameAccountAsync(gameAccount).ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs index 171dc8a9..7e77f7e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs @@ -126,7 +126,7 @@ internal class UserViewModel : ObservableObject infoBarService.Information($"此 Cookie 不完整,操作失败"); break; case UserOptionResult.Invalid: - infoBarService.Information($"此 Cookie 无法,操作失败"); + infoBarService.Information($"此 Cookie 无效,操作失败"); break; case UserOptionResult.Updated: infoBarService.Success($"用户 [{uid}] 更新成功"); @@ -162,7 +162,7 @@ internal class UserViewModel : ObservableObject Verify.Operation(user != null, "待复制 Cookie 的用户不应为 null"); try { - Clipboard.SetText(user.Cookie.ToString()); + Clipboard.SetText(user.Cookie!.ToString()); infoBarService.Success($"{user.UserInfo!.Nickname} 的 Cookie 复制成功"); } catch (Exception e) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs index 7e558fe3..a06b6775 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs @@ -68,7 +68,7 @@ internal static class HttpClientExtensions /// 客户端 internal static HttpClient SetUser(this HttpClient httpClient, User user) { - httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie.ToString()); + httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie!.ToString()); return httpClient; }