diff --git a/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs new file mode 100644 index 00000000..d267cb5d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs @@ -0,0 +1,25 @@ +namespace Snap.Hutao.Test; + +[TestClass] +public class CSharpLanguageFeatureTest +{ + [TestMethod] + public unsafe void NullStringFixedAlsoNullPointer() + { + string testStr = null!; + fixed(char* pStr = testStr) + { + Assert.IsTrue(pStr == null); + } + } + + [TestMethod] + public unsafe void EmptyStringFixedIsNullTerminator() + { + string testStr = string.Empty; + fixed (char* pStr = testStr) + { + Assert.IsTrue(*pStr == '\0'); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs index 4560c051..11ff2739 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs @@ -7,7 +7,7 @@ namespace Snap.Hutao.Test; public class DependencyInjectionTest { [TestMethod] - public void OriginalTypeDiscoverable() + public void OriginalTypeNotDiscoverable() { IServiceProvider services = new ServiceCollection() .AddSingleton() diff --git a/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj index f1b8a08c..b918f411 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj +++ b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj @@ -6,6 +6,7 @@ enable false true + True diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs index e798e0ea..6e3d0985 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs @@ -10,8 +10,6 @@ using Snap.Hutao.Win32; using System.IO; using Windows.Graphics; using Windows.UI; -using Windows.Win32.Foundation; -using WinRT.Interop; namespace Snap.Hutao.Core.Windowing; @@ -23,34 +21,22 @@ namespace Snap.Hutao.Core.Windowing; internal sealed class ExtendedWindow : IRecipient, IRecipient where TWindow : Window, IExtendedWindowSource { - private readonly HWND hwnd; - private readonly AppWindow appWindow; - - private readonly TWindow window; - private readonly FrameworkElement titleBar; + private readonly WindowOptions options; private readonly IServiceProvider serviceProvider; private readonly ILogger> logger; - private readonly WindowSubclassManager subclassManager; - - private readonly bool useLegacyDragBar; + private readonly WindowSubclass subclass; private SystemBackdrop? systemBackdrop; private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider) { - this.window = window; - this.titleBar = titleBar; + options = new(window, titleBar); + subclass = new(options); + logger = serviceProvider.GetRequiredService>>(); this.serviceProvider = serviceProvider; - hwnd = (HWND)WindowNative.GetWindowHandle(window); - WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); - appWindow = AppWindow.GetFromWindowId(windowId); - - useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported(); - subclassManager = new(window, hwnd, useLegacyDragBar); - InitializeWindow(); } @@ -79,63 +65,63 @@ internal sealed class ExtendedWindow : IRecipient public void Receive(FlyoutOpenCloseMessage message) { - UpdateDragRectangles(appWindow.TitleBar, message.IsOpen); + UpdateDragRectangles(options.AppWindow.TitleBar, message.IsOpen); } private void InitializeWindow() { - appWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version); - appWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico")); + options.AppWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version); + options.AppWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico")); ExtendsContentIntoTitleBar(); - Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize); + Persistence.RecoverOrInit(options); // appWindow.Show(true); // appWindow.Show can't bring window to top. - window.Activate(); + options.Window.Activate(); - systemBackdrop = new(window); + systemBackdrop = new(options.Window); bool micaApplied = systemBackdrop.TryApply(); logger.LogInformation("Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed"); - bool subClassApplied = subclassManager.TrySetWindowSubclass(); - logger.LogInformation("Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed"); + bool subClassApplied = subclass.Initialize(); + logger.LogInformation("Apply {name} : {result}", nameof(WindowSubclass), subClassApplied ? "succeed" : "failed"); - IMessenger messenger = Ioc.Default.GetRequiredService(); + IMessenger messenger = serviceProvider.GetRequiredService(); messenger.Register(this); messenger.Register(this); - window.Closed += OnWindowClosed; + options.Window.Closed += OnWindowClosed; } private void OnWindowClosed(object sender, WindowEventArgs args) { - if (window.PersistSize) + if (options.Window.PersistSize) { - Persistence.Save(appWindow); + Persistence.Save(options); } - subclassManager?.Dispose(); + subclass?.Dispose(); } private void ExtendsContentIntoTitleBar() { - if (useLegacyDragBar) + if (options.UseLegacyDragBarImplementation) { // use normal Window method to extend. - window.ExtendsContentIntoTitleBar = true; - window.SetTitleBar(titleBar); + options.Window.ExtendsContentIntoTitleBar = true; + options.Window.SetTitleBar(options.TitleBar); } else { - AppWindowTitleBar appTitleBar = appWindow.TitleBar; + AppWindowTitleBar appTitleBar = options.AppWindow.TitleBar; appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu; appTitleBar.ExtendsContentIntoTitleBar = true; UpdateTitleButtonColor(appTitleBar); UpdateDragRectangles(appTitleBar); - titleBar.ActualThemeChanged += (s, e) => UpdateTitleButtonColor(appTitleBar); - titleBar.SizeChanged += (s, e) => UpdateDragRectangles(appTitleBar); + options.TitleBar.ActualThemeChanged += (s, e) => UpdateTitleButtonColor(appTitleBar); + options.TitleBar.SizeChanged += (s, e) => UpdateDragRectangles(appTitleBar); } } @@ -168,20 +154,20 @@ internal sealed class ExtendedWindow : IRecipient /// 设置窗体位置 /// - /// 应用窗体 - /// 持久化尺寸 - /// 初始尺寸 - public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize) + /// 选项 + /// 窗体类型 + public static void RecoverOrInit(WindowOptions options) + where TWindow : Window, IExtendedWindowSource { // Set first launch size. - HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id); - SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd); + double scale = GetScaleForWindowHandle(options.Hwnd); + SizeInt32 transformedSize = options.Window.InitSize.Scale(scale); RectInt32 rect = StructMarshal.RectInt32(transformedSize); - if (persistSize) + if (options.Window.PersistSize) { RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect); - if (persistedRect.Size() >= initialSize.Size()) + if (persistedRect.Size() >= options.Window.InitSize.Size()) { rect = persistedRect; } } TransformToCenterScreen(ref rect); - appWindow.MoveAndResize(rect); + options.AppWindow.MoveAndResize(rect); } /// - /// 保存状态的位置 + /// 保存窗体的位置 /// - /// 应用窗体 - public static void Save(AppWindow appWindow) + /// 选项 + /// 窗体类型 + public static void Save(WindowOptions options) + where TWindow : Window, IExtendedWindowSource { - HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id); WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT(); - GetWindowPlacement(hwnd, ref windowPlacement); + GetWindowPlacement(options.Hwnd, ref windowPlacement); // prevent save value when we are maximized. if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED)) { - LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect()); + LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)options.AppWindow.GetRect()); } } @@ -70,13 +71,7 @@ internal static class Persistence public static double GetScaleForWindowHandle(HWND hwnd) { uint dpi = GetDpiForWindow(hwnd); - return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero); - } - - private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd) - { - double scale = GetScaleForWindowHandle(hwnd); - return new((int)(size.Width * scale), (int)(size.Height * scale)); + return Math.Round(dpi / 96D, 2, MidpointRounding.AwayFromZero); } private static void TransformToCenterScreen(ref RectInt32 rect) @@ -88,60 +83,41 @@ internal static class Persistence rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2); } - [StructLayout(LayoutKind.Explicit)] private struct CompactRect { - [FieldOffset(0)] public short X; - - [FieldOffset(2)] public short Y; - - [FieldOffset(4)] public short Width; - - [FieldOffset(6)] public short Height; - [FieldOffset(0)] - public ulong Value; - private CompactRect(int x, int y, int width, int height) { - Value = 0; X = (short)x; Y = (short)y; Width = (short)width; Height = (short)height; } - private CompactRect(ulong value) - { - X = 0; - Y = 0; - Width = 0; - Height = 0; - Value = value; - } - public static implicit operator RectInt32(CompactRect rect) { return new(rect.X, rect.Y, rect.Width, rect.Height); } - public static explicit operator CompactRect(ulong value) - { - return new(value); - } - public static explicit operator CompactRect(RectInt32 rect) { return new(rect.X, rect.Y, rect.Width, rect.Height); } - public static implicit operator ulong(CompactRect rect) + public static unsafe explicit operator CompactRect(ulong value) { - return rect.Value; + Unsafe.SkipInit(out CompactRect rect); + *(ulong*)&rect = value; + return rect; + } + + public static unsafe implicit operator ulong(CompactRect rect) + { + return *(ulong*)▭ } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs index 0bd620a9..b54653bd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/SystemBackdrop.cs @@ -8,6 +8,7 @@ using Snap.Hutao.Control.Theme; using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service; using System.Runtime.InteropServices; using Windows.System; using WinRT; @@ -34,9 +35,7 @@ internal sealed class SystemBackdrop this.window = window; using (IServiceScope scope = Ioc.Default.CreateScope()) { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString()); - BackdropType = Enum.Parse(entry.Value!); + BackdropType = scope.ServiceProvider.GetRequiredService().BackdropType; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs new file mode 100644 index 00000000..00866ee0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs @@ -0,0 +1,53 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Windows.Win32.Foundation; +using WinRT.Interop; + +namespace Snap.Hutao.Core.Windowing; + +/// +/// Window 选项 +/// +/// 窗体类型 +internal readonly struct WindowOptions + where TWindow : Window, IExtendedWindowSource +{ + /// + /// 窗体句柄 + /// + public readonly HWND Hwnd; + + /// + /// AppWindow + /// + public readonly AppWindow AppWindow; + + /// + /// 窗体 + /// + public readonly TWindow Window; + + /// + /// 标题栏元素 + /// + public readonly FrameworkElement TitleBar; + + /// + /// 是否使用 Win UI 3 自带的拓展标题栏实现 + /// + public readonly bool UseLegacyDragBarImplementation = !AppWindowTitleBar.IsCustomizationSupported(); + + public WindowOptions(TWindow window, FrameworkElement titleBar) + { + Window = window; + Hwnd = (HWND)WindowNative.GetWindowHandle(window); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(Hwnd); + AppWindow = AppWindow.GetFromWindowId(windowId); + + TitleBar = titleBar; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs similarity index 64% rename from src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs rename to src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs index 491f0c78..05539ce1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclass.cs @@ -14,16 +14,13 @@ namespace Snap.Hutao.Core.Windowing; /// /// 窗体类型 [HighQuality] -internal sealed class WindowSubclassManager : IDisposable +internal sealed class WindowSubclass : IDisposable where TWindow : Window, IExtendedWindowSource { private const int WindowSubclassId = 101; private const int DragBarSubclassId = 102; - private readonly TWindow window; - private readonly HWND hwnd; - private readonly bool isLegacyDragBar; - private HWND hwndDragBar; + private readonly WindowOptions options; // We have to explicitly hold a reference to SUBCLASSPROC private SUBCLASSPROC? windowProc; @@ -32,32 +29,28 @@ internal sealed class WindowSubclassManager : IDisposable /// /// 构造一个新的窗体子类管理器 /// - /// 窗体实例 - /// 窗体句柄 - /// 是否为经典标题栏区域 - public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar) + /// 选项 + public WindowSubclass(WindowOptions options) { - this.window = window; - this.hwnd = hwnd; - this.isLegacyDragBar = isLegacyDragBar; + this.options = options; } /// /// 尝试设置窗体子类 /// /// 是否设置成功 - public unsafe bool TrySetWindowSubclass() + public bool Initialize() { windowProc = new(OnSubclassProcedure); - bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0); + bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0); bool titleBarHooked = true; - // only hook up drag bar proc when not use legacy Window.ExtendsContentIntoTitleBar - if (isLegacyDragBar) + // only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar + if (options.UseLegacyDragBarImplementation) { titleBarHooked = false; - hwndDragBar = FindWindowEx(hwnd, default, "DRAG_BAR_WINDOW_CLASS", string.Empty); + HWND hwndDragBar = FindWindowEx(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default); if (!hwndDragBar.IsNull) { @@ -72,14 +65,14 @@ internal sealed class WindowSubclassManager : IDisposable /// public void Dispose() { - RemoveWindowSubclass(hwnd, windowProc, WindowSubclassId); - if (isLegacyDragBar) - { - RemoveWindowSubclass(hwnd, dragBarProc, DragBarSubclassId); - } - + RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId); windowProc = null; - dragBarProc = null; + + if (options.UseLegacyDragBarImplementation) + { + RemoveWindowSubclass(options.Hwnd, dragBarProc, DragBarSubclassId); + dragBarProc = null; + } } private unsafe LRESULT OnSubclassProcedure(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData) @@ -89,14 +82,14 @@ internal sealed class WindowSubclassManager : IDisposable case WM_GETMINMAXINFO: { double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd); - window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor); + options.Window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor); break; } case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: { - return new(0); + return (LRESULT)0; // WM_NULL } } @@ -110,7 +103,7 @@ internal sealed class WindowSubclassManager : IDisposable case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: { - return new(0); + return (LRESULT)0; // WM_NULL } } diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs index 749a2fc5..4a7d8e82 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/MemoryCacheExtension.cs @@ -17,7 +17,7 @@ internal static class MemoryCacheExtension /// 键 /// 值 /// 是否移除成功 - public static bool TryRemove(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out object? value) + public static bool TryRemove(this IMemoryCache memoryCache, string key, out object? value) { if (memoryCache.TryGetValue(key, out value)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs new file mode 100644 index 00000000..b4ca30ae --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Extension; + +/// +/// 对象拓展 +/// +internal static class ObjectExtension +{ + /// + /// 转换到只有1长度的数组 + /// + /// 数据类型 + /// 源 + /// 数组 + public static T[] ToArray(this T source) + { + return new[] { source }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/NativeMethods.json b/src/Snap.Hutao/Snap.Hutao/NativeMethods.json index 1694d869..f7b56332 100644 --- a/src/Snap.Hutao/Snap.Hutao/NativeMethods.json +++ b/src/Snap.Hutao/Snap.Hutao/NativeMethods.json @@ -1,5 +1,6 @@ { - "$schema": "https://aka.ms/CsWin32.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json", "allowMarshaling": true, - "public": true + "public": true, + "useSafeHandles": false } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt index 4c91863d..16a0d474 100644 --- a/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao/NativeMethods.txt @@ -3,6 +3,7 @@ INFINITE WM_GETMINMAXINFO WM_NCRBUTTONDOWN WM_NCRBUTTONUP +WM_NULL // Type & Enum definition CWMO_FLAGS 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 d5911f91..32735566 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -64,7 +64,20 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker memory = new byte[entry.modBaseSize]; fixed (byte* lpBuffer = memory) { - return ReadProcessMemory(process.SafeHandle, entry.modBaseAddr, lpBuffer, entry.modBaseSize, null); + bool hProcessAddRef = false; + try + { + process.SafeHandle.DangerousAddRef(ref hProcessAddRef); + HANDLE hProcessLocal = (HANDLE)process.SafeHandle.DangerousGetHandle(); + return ReadProcessMemory(hProcessLocal, entry.modBaseAddr, lpBuffer, entry.modBaseSize, null); + } + finally + { + if (hProcessAddRef) + { + process.SafeHandle.DangerousRelease(); + } + } } } @@ -72,7 +85,20 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker { int* lpBuffer = &write; - return WriteProcessMemory(process.SafeHandle, (void*)baseAddress, lpBuffer, sizeof(int), null); + bool hProcessAddRef = false; + try + { + process.SafeHandle.DangerousAddRef(ref hProcessAddRef); + HANDLE hProcessLocal = (HANDLE)process.SafeHandle.DangerousGetHandle(); + return WriteProcessMemory(hProcessLocal, (void*)baseAddress, lpBuffer, sizeof(int), null); + } + finally + { + if (hProcessAddRef) + { + process.SafeHandle.DangerousRelease(); + } + } } private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, ReadOnlySpan moduleName) diff --git a/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml b/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml index bb44dfe2..90a864a6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml @@ -35,15 +35,16 @@ - - + + + - - + + { - await summary.DownloadAndExtractAsync().ConfigureAwait(false); - ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); + if (await summary.DownloadAndExtractAsync().ConfigureAwait(false)) + { + ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary)); + } }).ConfigureAwait(true); serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage()); @@ -128,6 +131,7 @@ internal sealed class WelcomeViewModel : ObservableObject private readonly Progress progress; private string description = SH.ViewModelWelcomeDownloadSummaryDefault; private double progressValue; + private long updateCount; /// /// 构造一个新的下载信息 @@ -137,6 +141,8 @@ internal sealed class WelcomeViewModel : ObservableObject public DownloadSummary(IServiceProvider serviceProvider, string fileName) { httpClient = serviceProvider.GetRequiredService(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Core.CoreEnvironment.CommonUA); + this.serviceProvider = serviceProvider; DisplayName = fileName; @@ -171,7 +177,7 @@ internal sealed class WelcomeViewModel : ObservableObject /// 异步下载并解压 /// /// 任务 - public async Task DownloadAndExtractAsync() + public async Task DownloadAndExtractAsync() { ILogger logger = serviceProvider.GetRequiredService>(); try @@ -189,6 +195,7 @@ internal sealed class WelcomeViewModel : ObservableObject await ThreadHelper.SwitchToMainThreadAsync(); ProgressValue = 1; Description = SH.ViewModelWelcomeDownloadSummaryComplete; + return true; } } } @@ -197,13 +204,17 @@ internal sealed class WelcomeViewModel : ObservableObject logger.LogError(ex, "Download Static Zip failed"); await ThreadHelper.SwitchToMainThreadAsync(); Description = SH.ViewModelWelcomeDownloadSummaryException; + return false; } } private void UpdateProgressStatus(StreamCopyState status) { - Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}"; - ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes; + if (Interlocked.Increment(ref updateCount) % 40 == 0) + { + Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}"; + ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes; + } } private void ExtractFiles(Stream stream) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs new file mode 100644 index 00000000..4769d29f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs @@ -0,0 +1,75 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Web.Hutao.GachaLog; + +/// +/// 末尾Id 字典 +/// +internal sealed class EndIds +{ + /// + /// 新手祈愿 + /// + [JsonPropertyName("100")] + public long NoviceWish { get; set; } + + /// + /// 常驻祈愿 + /// + [JsonPropertyName("200")] + public long StandardWish { get; set; } + + /// + /// 角色活动祈愿 + /// + [JsonPropertyName("301")] + public long AvatarEventWish { get; set; } + + /// + /// 武器活动祈愿 + /// + [JsonPropertyName("302")] + public long WeaponEventWish { get; set; } + + /// + /// 获取 Last Id + /// + /// 类型 + /// Last Id + public long this[GachaConfigType type] + { + get + { + return type switch + { + GachaConfigType.NoviceWish => NoviceWish, + GachaConfigType.StandardWish => StandardWish, + GachaConfigType.AvatarEventWish => AvatarEventWish, + GachaConfigType.WeaponEventWish => WeaponEventWish, + _ => 0, + }; + } + + set + { + switch (type) + { + case GachaConfigType.NoviceWish: + NoviceWish = value; + break; + case GachaConfigType.StandardWish: + StandardWish = value; + break; + case GachaConfigType.AvatarEventWish: + AvatarEventWish = value; + break; + case GachaConfigType.WeaponEventWish: + WeaponEventWish = value; + break; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaItem.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaItem.cs new file mode 100644 index 00000000..d7c72a00 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaItem.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +namespace Snap.Hutao.Web.Hutao.GachaLog; + +/// +/// 服务器接口使用的祈愿记录物品 +/// +internal sealed class GachaItem +{ + /// + /// 祈愿记录分类 + /// + public GachaConfigType GachaType { get; set; } + + /// + /// 祈愿记录查询分类 + /// 合并保底的卡池使用此属性 + /// 仅4种(不含400) + /// + public GachaConfigType QueryType { get; set; } + + /// + /// 物品Id + /// + public int ItemId { get; set; } + + /// + /// 获取时间 + /// + public DateTimeOffset Time { get; set; } + + /// + /// Id + /// + public long Id { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs new file mode 100644 index 00000000..065ad9fb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs @@ -0,0 +1,145 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using Snap.Hutao.Web.Hutao.GachaLog; +using Snap.Hutao.Web.Response; +using System.Net.Http; + +namespace Snap.Hutao.Web.Hutao; + +/// +/// 胡桃祈愿记录API客户端 +/// +[HttpClient(HttpClientConfiguration.Default)] +internal sealed class HomaGachaLogClient +{ + private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + private readonly ILogger logger; + + /// + /// 构造一个新的胡桃祈愿记录API客户端 + /// + /// http客户端 + /// 服务提供器 + public HomaGachaLogClient(HttpClient httpClient, IServiceProvider serviceProvider) + { + options = serviceProvider.GetRequiredService(); + logger = serviceProvider.GetRequiredService>(); + + this.httpClient = httpClient; + + HutaoUserOptions hutaoUserOptions = serviceProvider.GetRequiredService(); + httpClient.DefaultRequestHeaders.Authorization = new("Bearer", hutaoUserOptions.Token); + } + + /// + /// 异步获取 Uid 列表 + /// + /// 取消令牌 + /// Uid 列表 + public async Task>> GetUidsAsync(CancellationToken token = default) + { + Response>? resp = await httpClient + .TryCatchGetFromJsonAsync>>(HutaoEndpoints.GachaLogUids, options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步获取末尾 Id + /// + /// uid + /// 取消令牌 + /// 末尾Id + public async Task> GetEndIdsAsync(PlayerUid uid, CancellationToken token = default) + { + Response? resp = await httpClient + .TryCatchGetFromJsonAsync>(HutaoEndpoints.GachaLogEndIds(uid.Value), options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步获取云端祈愿记录 + /// + /// uid + /// 末尾 Id + /// 取消令牌 + /// 云端祈愿记录 + public async Task>> RetrieveGachaItemsAsync(PlayerUid uid, EndIds endIds, CancellationToken token = default) + { + UidAndEndIds uidAndEndIds = new(uid, endIds); + + Response>? resp = await httpClient + .TryCatchPostAsJsonAsync>>(HutaoEndpoints.GachaLogRetrieve, uidAndEndIds, options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步上传祈愿记录物品 + /// + /// uid + /// 祈愿记录 + /// 取消令牌 + /// 响应 + public async Task UploadGachaItemsAsync(PlayerUid uid, List gachaItems, CancellationToken token = default) + { + UidAndItems uidAndItems = new(uid, gachaItems); + + Response.Response? resp = await httpClient + .TryCatchPostAsJsonAsync>>(HutaoEndpoints.GachaLogUpload, uidAndItems, options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步删除祈愿记录 + /// + /// uid + /// 取消令牌 + /// 响应 + public async Task DeleteGachaItemsAsync(PlayerUid uid, CancellationToken token = default) + { + Response.Response? resp = await httpClient + .TryCatchGetFromJsonAsync>>(HutaoEndpoints.GachaLogDelete(uid), options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + private sealed class UidAndEndIds + { + public UidAndEndIds(PlayerUid uid, EndIds endIds) + { + Uid = uid.Value; + EndIds = endIds; + } + + public string Uid { get; } + + public EndIds EndIds { get; } + } + + private sealed class UidAndItems + { + public UidAndItems(PlayerUid uid, List gachaItems) + { + Uid = uid.Value; + Items = gachaItems; + } + + public string Uid { get; set; } = default!; + + public List Items { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs index 07d6a55e..d1754e26 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Web.Hoyolab; + namespace Snap.Hutao.Web; /// @@ -16,11 +18,48 @@ internal static class HutaoEndpoints /// public const string StaticHutao = "static.hut.ao"; - #region Passport + #region GachaLog /// - /// 获取注册验证码 + /// 获取末尾Id /// + /// uid + /// 获取末尾Id Url + public static string GachaLogEndIds(PlayerUid uid) + { + return $"{HomaSnapGenshinApi}/GachaLog/EndIds?Uid={uid.Value}"; + } + + /// + /// 获取祈愿记录 + /// + public const string GachaLogRetrieve = $"{HomaSnapGenshinApi}/GachaLog/Retrieve"; + + /// + /// 上传祈愿记录 + /// + public const string GachaLogUpload = $"{HomaSnapGenshinApi}/GachaLog/Upload"; + + /// + /// 获取Uid列表 + /// + public const string GachaLogUids = $"{HomaSnapGenshinApi}/GachaLog/Uids"; + + /// + /// 删除祈愿记录 + /// + /// 删除祈愿记录 Url + public static string GachaLogDelete(PlayerUid uid) + { + return $"{HomaSnapGenshinApi}/GachaLog/Delete?Uid={uid.Value}"; + } + #endregion + + #region Passport + + /// + /// 获取注册验证码 + /// public const string PassportVerify = $"{HomaSnapGenshinApi}/Passport/Verify"; /// @@ -121,14 +160,6 @@ internal static class HutaoEndpoints } #endregion - #region Patcher - - /// - /// 胡桃检查更新 - /// - public const string PatcherHutaoStable = $"{PatcherDGPStudioApi}/hutao/stable"; - #endregion - #region Static & Zip /// @@ -170,7 +201,6 @@ internal static class HutaoEndpoints private const string HomaSnapGenshinApi = "https://homa.snapgenshin.com"; private const string HutaoMetadataSnapGenshinApi = "https://hutao-metadata.snapgenshin.com"; - private const string PatcherDGPStudioApi = "https://patcher.dgp-studio.cn"; private const string StaticSnapGenshinApi = "https://static.snapgenshin.com"; private const string StaticZipSnapGenshinApi = "https://static-zip.snapgenshin.com"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs b/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs index 149ac382..eae27ce2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs @@ -32,6 +32,17 @@ internal static class StructExtension return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale)); } + /// + /// 比例缩放 + /// + /// 源 + /// 比例 + /// 结果 + public static SizeInt32 Scale(this SizeInt32 sizeInt32, double scale) + { + return new((int)(sizeInt32.Width * scale), (int)(sizeInt32.Height * scale)); + } + /// /// 尺寸 ///