fix unlock fps

This commit is contained in:
Lightczx
2023-06-07 15:52:51 +08:00
parent 13e43a1c23
commit be4fa571cd
17 changed files with 392 additions and 88 deletions

View File

@@ -44,7 +44,7 @@
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">284</x:Double>
<GridLength x:Key="CompatGridLength2">268</GridLength>
<x:Double x:Key="HomeAdaptiveCardHeight">180</x:Double>

View File

@@ -6,14 +6,14 @@ namespace Snap.Hutao.Core.IO;
/// <summary>
/// 流复制状态
/// </summary>
internal sealed class StreamCopyState
internal sealed class StreamCopyStatus
{
/// <summary>
/// 构造一个新的流复制状态
/// </summary>
/// <param name="bytesCopied">已复制字节</param>
/// <param name="totalBytes">总字节数</param>
public StreamCopyState(long bytesCopied, long totalBytes)
public StreamCopyStatus(long bytesCopied, long totalBytes)
{
BytesCopied = bytesCopied;
TotalBytes = totalBytes;

View File

@@ -38,7 +38,7 @@ internal sealed class StreamCopyWorker
/// </summary>
/// <param name="progress">进度</param>
/// <returns>任务</returns>
public async Task CopyAsync(IProgress<StreamCopyState> progress)
public async Task CopyAsync(IProgress<StreamCopyStatus> progress)
{
long totalBytesRead = 0;
int bytesRead;

View File

@@ -17,6 +17,9 @@ CreateRemoteThread
CreateToolhelp32Snapshot
GetModuleHandleW
GetProcAddress
K32EnumProcessModules
K32GetModuleBaseName
K32GetModuleInformation
Module32First
Module32Next
ReadProcessMemory

View File

@@ -1428,6 +1428,51 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 在查找必要的模块时遇到问题:无法读取任何模块,可能是保护驱动已经加载完成 的本地化字符串。
/// </summary>
internal static string ServiceGameUnlockerFindModuleNoModuleFound {
get {
return ResourceManager.GetString("ServiceGameUnlockerFindModuleNoModuleFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 在查找必要的模块时遇到问题:查找模块超时 的本地化字符串。
/// </summary>
internal static string ServiceGameUnlockerFindModuleTimeLimitExeeded {
get {
return ResourceManager.GetString("ServiceGameUnlockerFindModuleTimeLimitExeeded", resourceCulture);
}
}
/// <summary>
/// 查找类似 在匹配内存时遇到问题:无法匹配到期望的内容 的本地化字符串。
/// </summary>
internal static string ServiceGameUnlockerInterestedPatternNotFound {
get {
return ResourceManager.GetString("ServiceGameUnlockerInterestedPatternNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 在读取必要的模块内存时遇到问题:无法将模块内存复制到指定位置 的本地化字符串。
/// </summary>
internal static string ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed {
get {
return ResourceManager.GetString("ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 在读取游戏进程内存时遇到问题:无法读取到指定地址的有效值 的本地化字符串。
/// </summary>
internal static string ServiceGameUnlockerReadProcessMemoryPointerAddressFailed {
get {
return ResourceManager.GetString("ServiceGameUnlockerReadProcessMemoryPointerAddressFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 无法找到缓存的元数据文件 的本地化字符串。
/// </summary>
@@ -1572,6 +1617,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 选择账号或直接启动 的本地化字符串。
/// </summary>
internal static string ViewCardLaunchGameSelectAccountPlaceholder {
get {
return ResourceManager.GetString("ViewCardLaunchGameSelectAccountPlaceholder", resourceCulture);
}
}
/// <summary>
/// 查找类似 等级 的本地化字符串。
/// </summary>

View File

@@ -2019,4 +2019,19 @@
<data name="ViewCardLaunchGameSelectAccountPlaceholder" xml:space="preserve">
<value>选择账号或直接启动</value>
</data>
<data name="ServiceGameUnlockerFindModuleNoModuleFound" xml:space="preserve">
<value>在查找必要的模块时遇到问题:无法读取任何模块,可能是保护驱动已经加载完成</value>
</data>
<data name="ServiceGameUnlockerFindModuleTimeLimitExeeded" xml:space="preserve">
<value>在查找必要的模块时遇到问题:查找模块超时</value>
</data>
<data name="ServiceGameUnlockerInterestedPatternNotFound" xml:space="preserve">
<value>在匹配内存时遇到问题:无法匹配到期望的内容</value>
</data>
<data name="ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed" xml:space="preserve">
<value>在读取必要的模块内存时遇到问题:无法将模块内存复制到指定位置</value>
</data>
<data name="ServiceGameUnlockerReadProcessMemoryPointerAddressFailed" xml:space="preserve">
<value>在读取游戏进程内存时遇到问题:无法读取到指定地址的有效值</value>
</data>
</root>

View File

@@ -289,7 +289,7 @@ internal sealed partial class GameService : IGameService
if (isAdvancedOptionsAllowed && launchOptions.UnlockFps)
{
await ProcessInterop.UnlockFpsAsync(serviceProvider, game).ConfigureAwait(false);
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, default).ConfigureAwait(false);
}
else
{

View File

@@ -54,12 +54,14 @@ internal static class ProcessInterop
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="game">游戏进程</param>
/// <param name="token">取消令牌</param>
/// <returns>任务</returns>
public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game)
public static Task UnlockFpsAsync(IServiceProvider serviceProvider, Process game, CancellationToken token)
{
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
UnlockTimingOptions options = new(100, 20000, 3000);
return unlocker.UnlockAsync(options);
Progress<UnlockerStatus> progress = new(); // TODO: do something.
return unlocker.UnlockAsync(options, progress, token);
}
/// <summary>

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 查找模块结果
/// </summary>
internal enum FindModuleResult
{
/// <summary>
/// 成功
/// </summary>
Ok,
/// <summary>
/// 超时
/// </summary>
TimeLimitExeeded,
/// <summary>
/// 模块尚未加载
/// </summary>
ModuleNotLoaded,
/// <summary>
/// 没有模块,保护驱动已加载,无法读取
/// </summary>
NoModuleFound,
}

View File

@@ -4,13 +4,11 @@
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using Windows.Win32.System.ProcessStatus;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Service.Game.Unlocker;
@@ -26,8 +24,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private readonly LaunchOptions launchOptions;
private readonly ILogger<GameFpsUnlocker> logger;
private nuint fpsAddress;
private bool isValid = true;
private UnlockerStatus status = new();
/// <summary>
/// 构造一个新的 <see cref="GameFpsUnlocker"/> 对象,
@@ -47,30 +44,31 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
}
/// <inheritdoc/>
public async Task UnlockAsync(UnlockTimingOptions options)
public async Task UnlockAsync(UnlockTimingOptions options, IProgress<UnlockerStatus> progress, CancellationToken token = default)
{
Verify.Operation(isValid, "This Unlocker is invalid");
Verify.Operation(status.IsUnlockerValid, "This Unlocker is invalid");
GameModuleEntryInfo moduleEntryInfo = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
Must.Argument(moduleEntryInfo.HasValue, "读取游戏内存失败");
(FindModuleResult result, GameModule moduleEntryInfo) = await FindModuleAsync(options.FindModuleDelay, options.FindModuleLimit).ConfigureAwait(false);
Verify.Operation(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
Verify.Operation(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound);
// Read UnityPlayer.dll
UnsafeTryReadModuleMemoryFindFpsAddress(moduleEntryInfo);
UnsafeFindFpsAddress(moduleEntryInfo);
// When player switch between scenes, we have to re adjust the fps
// So we keep a loop here
await LoopAdjustFpsAsync(options.AdjustFpsDelay).ConfigureAwait(false);
await LoopAdjustFpsAsync(options.AdjustFpsDelay, progress, token).ConfigureAwait(false);
}
private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModuleEntryInfo moduleEntryInfo, out VirtualMemory memory)
private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModule moduleEntryInfo, out VirtualMemory memory)
{
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
memory = new VirtualMemory(unityPlayer.modBaseSize + userAssembly.modBaseSize);
memory = new VirtualMemory(unityPlayer.Size + userAssembly.Size);
byte* lpBuffer = (byte*)memory.Pointer;
return ReadProcessMemory((HANDLE)process.Handle, unityPlayer.modBaseAddr, lpBuffer, unityPlayer.modBaseSize, default)
&& ReadProcessMemory((HANDLE)process.Handle, userAssembly.modBaseAddr, lpBuffer + unityPlayer.modBaseSize, userAssembly.modBaseSize, default);
return ReadProcessMemory((HANDLE)process.Handle, (void*)unityPlayer.Address, lpBuffer, unityPlayer.Size, default)
&& ReadProcessMemory((HANDLE)process.Handle, (void*)userAssembly.Address, lpBuffer + unityPlayer.Size, userAssembly.Size, default);
}
private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value)
@@ -79,40 +77,66 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, (byte*)&temp, 8, default);
if (!result)
{
ThrowHelper.InvalidOperation("读取进程内存失败", null);
ThrowHelper.InvalidOperation(SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed, null);
}
value = (nuint)temp;
return result;
}
private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int write)
private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int value)
{
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &write, sizeof(int), default);
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &value, sizeof(int), default);
}
private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, in ReadOnlySpan<byte> moduleName)
private static unsafe FindModuleResult UnsafeTryFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, out Module module)
{
CREATE_TOOLHELP_SNAPSHOT_FLAGS flags = CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE | CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE32;
HANDLE snapshot = CreateToolhelp32Snapshot(flags, (uint)processId);
try
HMODULE[] buffer = new HMODULE[128];
uint actualSize = 0;
fixed (HMODULE* pBuffer = buffer)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
foreach (MODULEENTRY32 entry in StructMarshal.EnumerateModuleEntry32(snapshot))
if (!K32EnumProcessModules(hProcess, pBuffer, unchecked((uint)(buffer.Length * sizeof(HMODULE))), out actualSize))
{
ReadOnlySpan<byte> szModuleNameLocal = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)&entry.szModule);
if (entry.th32ProcessID == processId && szModuleNameLocal.SequenceEqual(moduleName))
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
}
}
if (actualSize == 0)
{
module = default!;
return FindModuleResult.NoModuleFound;
}
Span<HMODULE> modules = new(buffer, 0, unchecked((int)(actualSize / sizeof(HMODULE))));
foreach (ref readonly HMODULE hModule in modules)
{
char[] baseName = new char[256];
fixed (char* lpBaseName = baseName)
{
if (K32GetModuleBaseName(hProcess, hModule, lpBaseName, 256) == 0)
{
return entry;
continue;
}
ReadOnlySpan<char> szModuleName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
if (!szModuleName.SequenceEqual(moduleName))
{
continue;
}
}
return default;
}
finally
{
CloseHandle(snapshot);
if (!K32GetModuleInformation(hProcess, hModule, out MODULEINFO moduleInfo, unchecked((uint)sizeof(MODULEINFO))))
{
continue;
}
module = new((nuint)moduleInfo.lpBaseOfDll, moduleInfo.SizeOfImage);
return FindModuleResult.Ok;
}
module = default;
return FindModuleResult.ModuleNotLoaded;
}
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
@@ -136,30 +160,43 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
return -1;
}
private static unsafe GameModuleEntryInfo UnsafeGetGameModuleEntryInfo(int processId)
private static unsafe FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out GameModule info)
{
MODULEENTRY32 unityPlayer = UnsafeFindModule(processId, "UnityPlayer.dll"u8);
MODULEENTRY32 userAssembly = UnsafeFindModule(processId, "UserAssembly.dll"u8);
FindModuleResult unityPlayerResult = UnsafeTryFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
FindModuleResult userAssemblyResult = UnsafeTryFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
if (unityPlayer.modBaseSize != 0 && userAssembly.modBaseSize != 0)
if (unityPlayerResult == FindModuleResult.Ok && userAssemblyResult == FindModuleResult.Ok)
{
return new(unityPlayer, userAssembly);
info = new(unityPlayer, userAssembly);
return FindModuleResult.Ok;
}
return default;
if (unityPlayerResult == FindModuleResult.NoModuleFound || userAssemblyResult == FindModuleResult.NoModuleFound)
{
info = default;
return FindModuleResult.NoModuleFound;
}
info = default;
return FindModuleResult.ModuleNotLoaded;
}
private async Task<GameModuleEntryInfo> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
private async Task<ValueResult<FindModuleResult, GameModule>> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
{
ValueStopwatch watch = ValueStopwatch.StartNew();
using (PeriodicTimer timer = new(findModuleDelay))
{
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
{
GameModuleEntryInfo moduleInfo = UnsafeGetGameModuleEntryInfo(gameProcess.Id);
if (moduleInfo.HasValue)
FindModuleResult result = UnsafeGetGameModuleInfo((HANDLE)gameProcess.Handle, out GameModule gameModule);
if (result == FindModuleResult.Ok)
{
return moduleInfo;
return new(FindModuleResult.Ok, gameModule);
}
if (result == FindModuleResult.NoModuleFound)
{
return new(FindModuleResult.NoModuleFound, default);
}
if (watch.GetElapsedTime() > findModuleLimit)
@@ -169,83 +206,94 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
}
}
return default;
return new(FindModuleResult.TimeLimitExeeded, default);
}
private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay)
private async Task LoopAdjustFpsAsync(TimeSpan adjustFpsDelay, IProgress<UnlockerStatus> progress, CancellationToken token)
{
using (PeriodicTimer timer = new(adjustFpsDelay))
{
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
if (!gameProcess.HasExited && fpsAddress != 0)
if (!gameProcess.HasExited && status.FpsAddress != 0U)
{
UnsafeWriteProcessMemory(gameProcess, fpsAddress, launchOptions.TargetFps);
UnsafeWriteProcessMemory(gameProcess, status.FpsAddress, launchOptions.TargetFps);
progress.Report(status);
}
else
{
isValid = false;
fpsAddress = 0;
status.IsUnlockerValid = false;
status.FpsAddress = 0;
progress.Report(status);
return;
}
}
}
}
private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(in GameModuleEntryInfo moduleEntryInfo)
private unsafe void UnsafeFindFpsAddress(in GameModule moduleEntryInfo)
{
bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory);
Verify.Operation(readOk, "读取内存失败");
Verify.Operation(readOk, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
using (localMemory)
{
int offset = IndexOfPattern(localMemory.GetBuffer()[(int)moduleEntryInfo.UnityPlayer.modBaseSize..]);
Must.Range(offset >= 0, "未匹配到FPS字节");
int offset = IndexOfPattern(localMemory.GetBuffer()[(int)moduleEntryInfo.UnityPlayer.Size..]);
Must.Range(offset >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
byte* pLocalMemory = (byte*)localMemory.Pointer;
MODULEENTRY32 unityPlayer = moduleEntryInfo.UnityPlayer;
MODULEENTRY32 userAssembly = moduleEntryInfo.UserAssembly;
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory;
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.modBaseSize;
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size;
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
rip += *(uint*)(rip + 1) + 5;
rip += *(uint*)(rip + 3) + 7;
nuint address = (nuint)userAssembly.modBaseAddr + (rip - localMemoryUserAssemblyAddress);
logger.LogInformation("Game Process handle: {handle}", gameProcess.Handle);
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
nuint ptr = 0;
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
logger.LogInformation("UnsafeReadProcessMemory succeed {addr:x8}", ptr);
rip = ptr - (nuint)unityPlayer.modBaseAddr + localMemoryUnityPlayerAddress;
logger.LogInformation("UnityPlayer addr: {up:x8}, rip addr: {rip:x8}", localMemoryUnityPlayerAddress, rip);
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
{
rip += (nuint)(*(int*)(rip + 1) + 5);
logger.LogInformation("rip ? addr: {rip:x8}", rip);
}
nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6;
nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
fpsAddress = (nuint)(unityPlayer.modBaseAddr + actualOffset);
logger.LogInformation("UnsafeTryReadModuleMemoryFindFpsAddress finished");
status.FpsAddress = unityPlayer.Address + actualOffset;
}
}
private readonly struct GameModuleEntryInfo
private readonly struct GameModule
{
public readonly bool HasValue = false;
public readonly MODULEENTRY32 UnityPlayer;
public readonly MODULEENTRY32 UserAssembly;
public readonly Module UnityPlayer;
public readonly Module UserAssembly;
public GameModuleEntryInfo(MODULEENTRY32 unityPlayer, MODULEENTRY32 userAssembly)
public GameModule(in Module unityPlayer, in Module userAssembly)
{
HasValue = true;
UnityPlayer = unityPlayer;
UserAssembly = userAssembly;
}
}
private readonly struct Module
{
public readonly bool HasValue = false;
public readonly nuint Address;
public readonly uint Size;
public Module(nuint address, uint size)
{
HasValue = true;
Address = address;
Size = size;
}
}
}

View File

@@ -13,6 +13,8 @@ internal interface IGameFpsUnlocker
/// 异步的解锁帧数限制
/// </summary>
/// <param name="options">选项</param>
/// <param name="progress">进度</param>
/// <param name="token">取消令牌</param>
/// <returns>解锁的结果</returns>
Task UnlockAsync(UnlockTimingOptions options);
Task UnlockAsync(UnlockTimingOptions options, IProgress<UnlockerStatus> progress, CancellationToken token = default);
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 解锁状态
/// </summary>
internal sealed class UnlockerStatus : ICloneable<UnlockerStatus>
{
/// <summary>
/// 状态描述
/// </summary>
public string Description { get; set; } = default!;
/// <summary>
/// 查找模块状态
/// </summary>
public FindModuleResult FindModuleState { get; set; }
/// <summary>
/// 当前解锁器是否有效
/// </summary>
public bool IsUnlockerValid { get; set; } = true;
/// <summary>
/// FPS 字节地址
/// </summary>
public nuint FpsAddress { get; set; }
public UnlockerStatus Clone()
{
throw new NotImplementedException();
}
}

View File

@@ -6,10 +6,12 @@
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shvconv="using:Snap.Hutao.View.Converter"
xmlns:shvcoont="using:Snap.Hutao.View.Control"
xmlns:shvg="using:Snap.Hutao.ViewModel.GachaLog"
d:DataContext="{d:DesignInstance shvg:TypedWishSummary}"
mc:Ignorable="d">
@@ -19,15 +21,35 @@
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
<shvconv:Int32ToGradientColorConverter x:Key="Int32ToGradientColorConverter" MaximumValue="{Binding GuaranteeOrangeThreshold}"/>
<shc:BindingProxy x:Key="SummaryProxy" DataContext="{Binding}"/>
<DataTemplate x:Key="OrangeListTemplate" d:DataType="shvg:SummaryItem">
<Grid Margin="0,4,4,0" Background="Transparent">
<Grid Margin="0,4,0,0" Background="Transparent">
<ToolTipService.ToolTip>
<TextBlock Text="{Binding TimeFormatted}"/>
</ToolTipService.ToolTip>
<Border Style="{StaticResource BorderCardStyle}">
<ProgressBar
MinHeight="32"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="{StaticResource CompatCornerRadius}"
Maximum="{Binding Path=DataContext.GuaranteeOrangeThreshold, Source={StaticResource SummaryProxy}}"
Opacity="0.2"
Value="{Binding LastPull}">
<ProgressBar.Foreground>
<SolidColorBrush Color="{Binding LastPull, Converter={StaticResource Int32ToGradientColorConverter}}"/>
</ProgressBar.Foreground>
</ProgressBar>
</Border>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<shci:CachedImage
Width="32"
Height="32"
CornerRadius="{ThemeResource ControlCornerRadius}"
Source="{Binding Icon}"/>
<TextBlock
Margin="8,0,0,0"
@@ -55,6 +77,7 @@
<TextBlock
Width="24"
Margin="0,0,8,0"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding LastPull}"
@@ -76,7 +99,7 @@
Style="{StaticResource BorderCardStyle}"
ToolTipService.ToolTip="{Binding TimeFormatted}">
<StackPanel>
<shvc:ItemIcon
<shvcoont:ItemIcon
Width="40"
Height="40"
Icon="{Binding Icon}"

View File

@@ -0,0 +1,82 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Control;
using Snap.Hutao.Win32;
using System.Runtime.CompilerServices;
using Windows.UI;
namespace Snap.Hutao.View.Converter;
/// <summary>
/// Int32 转 色阶颜色
/// </summary>
internal sealed class Int32ToGradientColorConverter : DependencyObject, IValueConverter
{
private static readonly DependencyProperty MaximumProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(Maximum), StructMarshal.Color(0xFFFF4949));
private static readonly DependencyProperty MinimumProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(Minimum), StructMarshal.Color(0xFF48FF7A));
private static readonly DependencyProperty MaximumValueProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(MaximumValue), 90);
private static readonly DependencyProperty MinimumValueProperty = Property<Int32ToGradientColorConverter>.Depend(nameof(MinimumValue), 1);
/// <summary>
/// 最小颜色
/// </summary>
public Color Minimum
{
get => (Color)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
}
/// <summary>
/// 最大颜色
/// </summary>
public Color Maximum
{
get => (Color)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
/// <summary>
/// 最小值
/// </summary>
public int MinimumValue
{
get => (int)GetValue(MinimumValueProperty);
set => SetValue(MinimumValueProperty, value);
}
/// <summary>
/// 最大值
/// </summary>
public int MaximumValue
{
get => (int)GetValue(MaximumValueProperty);
set => SetValue(MaximumValueProperty, value);
}
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
double n = (value != null ? (int)value : MinimumValue) - MinimumValue;
int step = MaximumValue - MinimumValue;
double a = Minimum.A + ((Maximum.A - Minimum.A) * n / step);
double r = Minimum.R + ((Maximum.R - Minimum.R) * n / step);
double g = Minimum.G + ((Maximum.G - Minimum.G) * n / step);
double b = Minimum.B + ((Maximum.B - Minimum.B) * n / step);
Unsafe.SkipInit(out Color color);
color.A = (byte)a;
color.R = (byte)r;
color.G = (byte)g;
color.B = (byte)b;
return color;
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -15,7 +15,7 @@ internal sealed class Int32ToVisibilityRevertConverter : IValueConverter
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
return (int)value == 0 ? Visibility.Visible : Visibility.Collapsed;
return value != null && (int)value == 0 ? Visibility.Visible : Visibility.Collapsed;
}
/// <inheritdoc/>

View File

@@ -41,13 +41,15 @@
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding FinishDescription}"/>
<CommandBar Grid.Column="1" DefaultLabelPosition="Right">
<CommandBar
Grid.Column="1"
Margin="16,0,0,0"
DefaultLabelPosition="Right">
<CommandBar.Content>
<AutoSuggestBox
Width="240"
Height="36"
Margin="12,6,6,0"
Margin="3,6,6,0"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
PlaceholderText="{shcm:ResourceString Name=ViewPageAchievementSearchPlaceholder}"
@@ -123,6 +125,11 @@
ItemsSource="{Binding AchievementGoals}"
SelectedItem="{Binding SelectedAchievementGoal, Mode=TwoWay}"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="Margin" Value="0,0,6,0"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="0,6">
@@ -157,7 +164,7 @@
<SplitView.Content>
<ScrollViewer Padding="0,0,16,0">
<ItemsControl
Margin="16,0,0,16"
Margin="8,0,0,16"
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
ItemsSource="{Binding Achievements}">
<ItemsControl.ItemContainerTransitions>
@@ -229,6 +236,7 @@
</SplitView.Content>
</SplitView>
</Grid>
<Grid Visibility="{Binding SelectedArchive, Converter={StaticResource EmptyObjectToVisibilityRevertConverter}}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Image

View File

@@ -120,7 +120,7 @@ internal sealed partial class WelcomeViewModel : ObservableObject
private readonly HttpClient httpClient;
private readonly string fileName;
private readonly string fileUrl;
private readonly Progress<StreamCopyState> progress;
private readonly Progress<StreamCopyStatus> progress;
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
private double progressValue;
private long updateCount;
@@ -196,7 +196,7 @@ internal sealed partial class WelcomeViewModel : ObservableObject
}
}
private void UpdateProgressStatus(StreamCopyState status)
private void UpdateProgressStatus(StreamCopyStatus status)
{
if (Interlocked.Increment(ref updateCount) % 40 == 0)
{