mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
1 Commits
fix/titleb
...
fix/compos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e48ce1dd3b |
24
build.cake
24
build.cake
@@ -69,15 +69,6 @@ else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
})[..^2];
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
else // Local
|
||||
{
|
||||
repoDir = System.Environment.CurrentDirectory;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
version = System.DateTime.Now.ToString("yyyy.M.d.") + ((int)((System.DateTime.Now - System.DateTime.Today).TotalSeconds / 86400 * 65535)).ToString();
|
||||
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
Task("Build")
|
||||
.IsDependentOn("Build binary package")
|
||||
@@ -121,17 +112,6 @@ Task("Generate AppxManifest")
|
||||
Information("Using Release configuration");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
Information("Using Local configuration.");
|
||||
content = content
|
||||
.Replace("Snap Hutao", "Snap Hutao Local")
|
||||
.Replace("胡桃", "胡桃 Local")
|
||||
.Replace("DGP Studio", "DGP Studio CI");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"E8B6E2B3-D2A0-4435-A81D-2A16AAF405C7\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
|
||||
}
|
||||
|
||||
System.IO.File.WriteAllText(manifest, content);
|
||||
|
||||
@@ -193,10 +173,6 @@ Task("Build MSIX")
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
|
||||
}
|
||||
var p = StartProcess(
|
||||
"makeappx.exe",
|
||||
new ProcessSettings
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double))]
|
||||
[DependencyProperty("Spacing", typeof(double))]
|
||||
internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
{
|
||||
public HorizontalEqualPanel()
|
||||
{
|
||||
Loaded += OnLoaded;
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
foreach (UIElement child in Children)
|
||||
{
|
||||
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
|
||||
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
|
||||
double childAvailableWidth = (availableWidth + Spacing) / Children.Count;
|
||||
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
|
||||
child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
|
||||
}
|
||||
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
int itemCount = Children.Count;
|
||||
double availableWidthPerItem = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
|
||||
double actualItemWidth = Math.Max(MinItemWidth, availableWidthPerItem);
|
||||
|
||||
double offset = 0;
|
||||
foreach (UIElement child in Children)
|
||||
{
|
||||
child.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
|
||||
offset += actualItemWidth + Spacing;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MinWidth = (MinItemWidth * Children.Count) + (Spacing * (Children.Count - 1));
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,21 @@
|
||||
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<x:Double x:Key="CompatShadowThemeOpacity">0.14</x:Double>
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="0.14"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<x:Double x:Key="CompatShadowThemeOpacity">0.28</x:Double>
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="0.28"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="{ThemeResource CompatShadowThemeOpacity}"
|
||||
Offset="0,4,0"/>
|
||||
|
||||
<Style x:Key="BorderCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing0Template">
|
||||
<cwcont:WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing2Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="2" VerticalSpacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Theme;
|
||||
|
||||
internal static class SystemColors
|
||||
{
|
||||
public static Color BaseLowColor(bool isDarkMode)
|
||||
{
|
||||
return isDarkMode ? StructMarshal.Color(0x33FFFFFF) : StructMarshal.Color(0x33000000);
|
||||
}
|
||||
|
||||
public static Color BaseMediumLowColor(bool isDarkMode)
|
||||
{
|
||||
return isDarkMode ? StructMarshal.Color(0x66FFFFFF) : StructMarshal.Color(0x66000000);
|
||||
}
|
||||
|
||||
public static Color BaseHighColor(bool isDarkMode)
|
||||
{
|
||||
return isDarkMode ? StructMarshal.Color(0xFFFFFFFF) : StructMarshal.Color(0xFF000000);
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,6 @@ internal sealed class HutaoException : Exception
|
||||
}
|
||||
}
|
||||
|
||||
public static void ThrowIfNot(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
}
|
||||
}
|
||||
|
||||
public static HutaoException ServiceTypeCastFailed<TFrom, TTo>(string name, Exception? innerException = default)
|
||||
{
|
||||
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||
|
||||
@@ -6,15 +6,8 @@ namespace Snap.Hutao.Core.ExceptionService;
|
||||
internal enum HutaoExceptionKind
|
||||
{
|
||||
None,
|
||||
|
||||
// Foundation
|
||||
ServiceTypeCastFailed,
|
||||
|
||||
// IO
|
||||
FileSystemCreateFileInsufficientPermissions,
|
||||
PrivateNamedPipeContentHashIncorrect,
|
||||
|
||||
// Service
|
||||
GachaStatisticsInvalidItemId,
|
||||
GameFpsUnlockingFailed,
|
||||
}
|
||||
@@ -9,7 +9,6 @@ namespace Snap.Hutao.Core.Validation;
|
||||
/// 封装验证方法,简化微软验证
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Obsolete("Use HutaoException instead")]
|
||||
internal static class Must
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -13,7 +13,6 @@ using Snap.Hutao.Win32;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.Graphics.Dwm;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.Collections.Frozen;
|
||||
using System.IO;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
@@ -57,22 +56,25 @@ internal sealed class WindowController
|
||||
private void InitializeCore()
|
||||
{
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
|
||||
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
|
||||
window.AppWindow.SetIcon(Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico"));
|
||||
ExtendsContentIntoTitleBar();
|
||||
|
||||
RecoverOrInitWindowSize();
|
||||
UpdateElementTheme(appOptions.ElementTheme);
|
||||
UpdateImmersiveDarkMode(options.TitleBar, default!);
|
||||
|
||||
// appWindow.Show(true);
|
||||
// appWindow.Show can't bring window to top.
|
||||
window.Activate();
|
||||
options.BringToForeground();
|
||||
UpdateSystemBackdrop(appOptions.BackdropType);
|
||||
appOptions.PropertyChanged += OnOptionsPropertyChanged;
|
||||
|
||||
if (options.UseSystemBackdrop)
|
||||
{
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
UpdateSystemBackdrop(appOptions.BackdropType);
|
||||
appOptions.PropertyChanged += OnOptionsPropertyChanged;
|
||||
}
|
||||
|
||||
subclass.Initialize();
|
||||
|
||||
@@ -188,12 +190,12 @@ internal sealed class WindowController
|
||||
appTitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
appTitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||
|
||||
bool isDarkMode = Control.Theme.ThemeHelper.IsDarkMode(options.TitleBar.ActualTheme);
|
||||
IAppResourceProvider resourceProvider = serviceProvider.GetRequiredService<IAppResourceProvider>();
|
||||
|
||||
Color systemBaseLowColor = Control.Theme.SystemColors.BaseLowColor(isDarkMode);
|
||||
Color systemBaseLowColor = resourceProvider.GetResource<Color>("SystemBaseLowColor");
|
||||
appTitleBar.ButtonHoverBackgroundColor = systemBaseLowColor;
|
||||
|
||||
Color systemBaseMediumLowColor = Control.Theme.SystemColors.BaseMediumLowColor(isDarkMode);
|
||||
Color systemBaseMediumLowColor = resourceProvider.GetResource<Color>("SystemBaseMediumLowColor");
|
||||
appTitleBar.ButtonPressedBackgroundColor = systemBaseMediumLowColor;
|
||||
|
||||
// The Foreground doesn't accept Alpha channel. So we translate it to gray.
|
||||
@@ -201,7 +203,7 @@ internal sealed class WindowController
|
||||
byte result = (byte)((systemBaseMediumLowColor.A / 255.0) * light);
|
||||
appTitleBar.ButtonInactiveForegroundColor = Color.FromArgb(0xFF, result, result, result);
|
||||
|
||||
Color systemBaseHighColor = Control.Theme.SystemColors.BaseHighColor(isDarkMode);
|
||||
Color systemBaseHighColor = resourceProvider.GetResource<Color>("SystemBaseHighColor");
|
||||
appTitleBar.ButtonForegroundColor = systemBaseHighColor;
|
||||
appTitleBar.ButtonHoverForegroundColor = systemBaseHighColor;
|
||||
appTitleBar.ButtonPressedForegroundColor = systemBaseHighColor;
|
||||
|
||||
@@ -41,18 +41,21 @@ internal readonly struct WindowOptions
|
||||
/// </summary>
|
||||
public readonly bool PersistSize;
|
||||
|
||||
public readonly bool UseSystemBackdrop;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 Win UI 3 自带的拓展标题栏实现
|
||||
/// </summary>
|
||||
public readonly bool UseLegacyDragBarImplementation = !AppWindowTitleBar.IsCustomizationSupported();
|
||||
|
||||
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
|
||||
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false, bool useSystemBackdrop = true)
|
||||
{
|
||||
Hwnd = WindowNative.GetWindowHandle(window);
|
||||
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
|
||||
TitleBar = titleBar;
|
||||
InitSize = initSize;
|
||||
PersistSize = persistSize;
|
||||
UseSystemBackdrop = useSystemBackdrop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2381,9 +2381,6 @@
|
||||
<data name="ViewPageSettingBackgroundImageHeader" xml:space="preserve">
|
||||
<value>背景图片</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingBackgroundImageLocalFolderCopyrightHeader" xml:space="preserve">
|
||||
<value>当前背景为本地图片,如遇版权问题由用户个人负责</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingCacheFolderDescription" xml:space="preserve">
|
||||
<value>图片缓存 在此处存放</value>
|
||||
</data>
|
||||
|
||||
@@ -18,9 +18,9 @@ internal sealed class GachaTypeComparer : IComparer<GachaType>
|
||||
KeyValuePair.Create(GachaType.ActivityAvatar, 0),
|
||||
KeyValuePair.Create(GachaType.SpecialActivityAvatar, 1),
|
||||
KeyValuePair.Create(GachaType.ActivityWeapon, 2),
|
||||
KeyValuePair.Create(GachaType.ActivityCity, 3),
|
||||
KeyValuePair.Create(GachaType.Standard, 4),
|
||||
KeyValuePair.Create(GachaType.NewBie, 5),
|
||||
KeyValuePair.Create(GachaType.Standard, 3),
|
||||
KeyValuePair.Create(GachaType.NewBie, 4),
|
||||
KeyValuePair.Create(GachaType.ActivityCity, 5),
|
||||
]);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -38,10 +38,6 @@ internal sealed class PullPrediction
|
||||
typedWishSummary.PredictedPullLeftToOrange = result.PredictedPullLeftToOrange;
|
||||
typedWishSummary.IsPredictPullAvailable = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await barrier.SignalAndWaitAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static PredictResult PredictCore(List<PullCount> distribution, TypedWishSummary typedWishSummary)
|
||||
|
||||
@@ -109,7 +109,7 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, false);
|
||||
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
|
||||
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ internal sealed class LaunchStatus
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public static LaunchStatus FromUnlockState(GameFpsUnlockerState unlockerState)
|
||||
public static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus)
|
||||
{
|
||||
if (unlockerState.FindModuleResult == FindModuleResult.Ok)
|
||||
if (unlockerStatus.FindModuleState == FindModuleResult.Ok)
|
||||
{
|
||||
return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ internal sealed class LaunchExecutionBetterGenshinImpactAutomationHandlder : ILa
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
context.Logger.LogInformation("Failed to wait Input idle waiting");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public static bool IsGameRunning([NotNullWhen(true)] out Process? runningProcess)
|
||||
public static bool IsGameRunning([NotNullWhen(true)] out System.Diagnostics.Process? runningProcess)
|
||||
{
|
||||
int currentSessionId = Process.GetCurrentProcess().SessionId;
|
||||
|
||||
// GetProcesses once and manually loop is O(n)
|
||||
foreach (ref readonly Process process in Process.GetProcesses().AsSpan())
|
||||
foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
|
||||
{
|
||||
if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (process.SessionId != currentSessionId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
runningProcess = process;
|
||||
return true;
|
||||
}
|
||||
@@ -33,7 +24,7 @@ internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecut
|
||||
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (IsGameRunning(out Process? process))
|
||||
if (IsGameRunning(out System.Diagnostics.Process? process))
|
||||
{
|
||||
context.Logger.LogInformation("Game process detected, id: {Id}", process.Id);
|
||||
|
||||
|
||||
@@ -18,13 +18,12 @@ internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegate
|
||||
context.Progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
|
||||
|
||||
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
|
||||
IProgress<GameFpsUnlockerState> progress = progressFactory.CreateForMainThread<GameFpsUnlockerState>(status => context.Progress.Report(LaunchStatus.FromUnlockState(status)));
|
||||
GameFpsUnlocker unlocker = new(context.ServiceProvider, context.Process, new(100, 20000, 3000), progress);
|
||||
IProgress<UnlockerStatus> progress = progressFactory.CreateForMainThread<UnlockerStatus>(status => context.Progress.Report(LaunchStatus.FromUnlockStatus(status)));
|
||||
GameFpsUnlocker unlocker = context.ServiceProvider.CreateInstance<GameFpsUnlocker>(context.Process);
|
||||
|
||||
try
|
||||
{
|
||||
await unlocker.UnlockAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
unlocker.PostUnlockAsync(context.CancellationToken).SafeForget();
|
||||
await unlocker.UnlockAsync(new(100, 20000, 3000), progress, context.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
|
||||
@@ -24,25 +24,25 @@ internal sealed class LaunchExecutionInvoker
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessStartHandler());
|
||||
handlers.Enqueue(new LaunchExecutionUnlockFpsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionStarwardPlayTimeStatisticsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionBetterGenshinImpactAutomationHandlder());
|
||||
handlers.Enqueue(new LaunchExecutionUnlockFpsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessExitHandler());
|
||||
}
|
||||
|
||||
public async ValueTask<LaunchExecutionResult> InvokeAsync(LaunchExecutionContext context)
|
||||
{
|
||||
await RecursiveInvokeHandlerAsync(context).ConfigureAwait(false);
|
||||
await InvokeHandlerAsync(context).ConfigureAwait(false);
|
||||
return context.Result;
|
||||
}
|
||||
|
||||
private async ValueTask<LaunchExecutionContext> RecursiveInvokeHandlerAsync(LaunchExecutionContext context)
|
||||
private async ValueTask<LaunchExecutionContext> InvokeHandlerAsync(LaunchExecutionContext context)
|
||||
{
|
||||
if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler))
|
||||
{
|
||||
string typeName = TypeNameHelper.GetTypeDisplayName(handler, false);
|
||||
context.Logger.LogInformation("Handler [{Handler}] begin execution", typeName);
|
||||
await handler.OnExecutionAsync(context, () => RecursiveInvokeHandlerAsync(context)).ConfigureAwait(false);
|
||||
await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false);
|
||||
context.Logger.LogInformation("Handler [{Handler}] end execution", typeName);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.Memory;
|
||||
using System.Diagnostics;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
internal static class GameFpsAddress
|
||||
{
|
||||
#pragma warning disable SA1310
|
||||
private const byte ASM_CALL = 0xE8;
|
||||
private const byte ASM_JMP = 0xE9;
|
||||
#pragma warning restore SA1310
|
||||
|
||||
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerState state, in RequiredGameModule requiredGameModule)
|
||||
{
|
||||
bool readOk = UnsafeReadModulesMemory(state.GameProcess, requiredGameModule, out VirtualMemory localMemory);
|
||||
HutaoException.ThrowIfNot(readOk, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
|
||||
|
||||
using (localMemory)
|
||||
{
|
||||
int offset = IndexOfPattern(localMemory.AsSpan()[(int)requiredGameModule.UnityPlayer.Size..]);
|
||||
HutaoException.ThrowIfNot(offset >= 0, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
|
||||
byte* pLocalMemory = (byte*)localMemory.Pointer;
|
||||
ref readonly Module unityPlayer = ref requiredGameModule.UnityPlayer;
|
||||
ref readonly Module userAssembly = ref requiredGameModule.UserAssembly;
|
||||
|
||||
nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory;
|
||||
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size;
|
||||
|
||||
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
|
||||
rip += 5U;
|
||||
rip += (nuint)(*(int*)(rip + 2U) + 6);
|
||||
|
||||
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
|
||||
|
||||
nuint ptr = 0;
|
||||
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(state.GameProcess, address, out ptr) && ptr != 0);
|
||||
|
||||
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
|
||||
|
||||
while (*(byte*)rip is ASM_CALL or ASM_JMP)
|
||||
{
|
||||
rip += (nuint)(*(int*)(rip + 1) + 5);
|
||||
}
|
||||
|
||||
nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6;
|
||||
nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
|
||||
state.FpsAddress = unityPlayer.Address + actualOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
|
||||
{
|
||||
// B9 3C 00 00 00 FF 15
|
||||
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
|
||||
return memory.IndexOf(part);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadModulesMemory(Process process, in RequiredGameModule moduleEntryInfo, out VirtualMemory memory)
|
||||
{
|
||||
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
|
||||
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
|
||||
|
||||
memory = new VirtualMemory(unityPlayer.Size + userAssembly.Size);
|
||||
return ReadProcessMemory(process.Handle, (void*)unityPlayer.Address, memory.AsSpan()[..(int)unityPlayer.Size], out _)
|
||||
&& ReadProcessMemory(process.Handle, (void*)userAssembly.Address, memory.AsSpan()[(int)unityPlayer.Size..], out _);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value)
|
||||
{
|
||||
value = 0;
|
||||
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _);
|
||||
HutaoException.ThrowIfNot(result, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using System.Diagnostics;
|
||||
using Snap.Hutao.Win32.Memory;
|
||||
using Snap.Hutao.Win32.System.ProcessStatus;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
@@ -15,54 +17,255 @@ namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
[HighQuality]
|
||||
internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
{
|
||||
private readonly System.Diagnostics.Process gameProcess;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly GameFpsUnlockerState state = new();
|
||||
private readonly UnlockerStatus status = new();
|
||||
|
||||
public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockTimingOptions options, IProgress<GameFpsUnlockerState> progress)
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="GameFpsUnlocker"/> 对象,
|
||||
/// 每个解锁器只能解锁一次原神的进程,
|
||||
/// 再次解锁需要重新创建对象
|
||||
/// <para/>
|
||||
/// 解锁器需要在管理员模式下才能正确的完成解锁操作,
|
||||
/// 非管理员模式不能解锁
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="gameProcess">游戏进程</param>
|
||||
public GameFpsUnlocker(IServiceProvider serviceProvider, System.Diagnostics.Process gameProcess)
|
||||
{
|
||||
launchOptions = serviceProvider.GetRequiredService<LaunchOptions>();
|
||||
|
||||
state.GameProcess = gameProcess;
|
||||
state.TimingOptions = options;
|
||||
state.Progress = progress;
|
||||
this.gameProcess = gameProcess;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask UnlockAsync(CancellationToken token = default)
|
||||
public async ValueTask UnlockAsync(UnlockTimingOptions options, IProgress<UnlockerStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
HutaoException.ThrowIfNot(state.IsUnlockerValid, HutaoExceptionKind.GameFpsUnlockingFailed, "This Unlocker is invalid");
|
||||
(FindModuleResult result, RequiredGameModule gameModule) = await GameProcessModule.FindModuleAsync(state).ConfigureAwait(false);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleNoModuleFound);
|
||||
Verify.Operation(status.IsUnlockerValid, "This Unlocker is invalid");
|
||||
|
||||
GameFpsAddress.UnsafeFindFpsAddress(state, gameModule);
|
||||
state.Report();
|
||||
(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
|
||||
UnsafeFindFpsAddress(moduleEntryInfo);
|
||||
progress.Report(status);
|
||||
|
||||
// When player switch between scenes, we have to re adjust the fps
|
||||
// So we keep a loop here
|
||||
await LoopAdjustFpsAsync(options.AdjustFpsDelay, progress, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask PostUnlockAsync(CancellationToken token = default)
|
||||
private static unsafe bool UnsafeReadModulesMemory(System.Diagnostics.Process process, in GameModule moduleEntryInfo, out VirtualMemory memory)
|
||||
{
|
||||
using (PeriodicTimer timer = new(state.TimingOptions.AdjustFpsDelay))
|
||||
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
|
||||
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
|
||||
|
||||
memory = new VirtualMemory(unityPlayer.Size + userAssembly.Size);
|
||||
return ReadProcessMemory(process.Handle, (void*)unityPlayer.Address, memory.AsSpan()[..(int)unityPlayer.Size], out _)
|
||||
&& ReadProcessMemory(process.Handle, (void*)userAssembly.Address, memory.AsSpan()[(int)unityPlayer.Size..], out _);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadProcessMemory(System.Diagnostics.Process process, nuint baseAddress, out nuint value)
|
||||
{
|
||||
value = 0;
|
||||
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _);
|
||||
Verify.Operation(result, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeWriteProcessMemory(System.Diagnostics.Process process, nuint baseAddress, int value)
|
||||
{
|
||||
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _);
|
||||
}
|
||||
|
||||
private static unsafe FindModuleResult UnsafeTryFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, out Module module)
|
||||
{
|
||||
HMODULE[] buffer = new HMODULE[128];
|
||||
if (!K32EnumProcessModules(hProcess, buffer, out uint actualSize))
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
}
|
||||
|
||||
if (actualSize == 0)
|
||||
{
|
||||
module = default!;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
foreach (ref readonly HMODULE hModule in buffer.AsSpan()[..(int)(actualSize / sizeof(HMODULE))])
|
||||
{
|
||||
char[] baseName = new char[256];
|
||||
|
||||
if (K32GetModuleBaseNameW(hProcess, hModule, baseName) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fixed (char* lpBaseName = baseName)
|
||||
{
|
||||
ReadOnlySpan<char> szModuleName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
|
||||
if (!szModuleName.SequenceEqual(moduleName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!K32GetModuleInformation(hProcess, hModule, out MODULEINFO 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)
|
||||
{
|
||||
// B9 3C 00 00 00 FF 15
|
||||
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
|
||||
return memory.IndexOf(part);
|
||||
}
|
||||
|
||||
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out GameModule info)
|
||||
{
|
||||
FindModuleResult unityPlayerResult = UnsafeTryFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
|
||||
FindModuleResult userAssemblyResult = UnsafeTryFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
|
||||
|
||||
if (unityPlayerResult == FindModuleResult.Ok && userAssemblyResult == FindModuleResult.Ok)
|
||||
{
|
||||
info = new(unityPlayer, userAssembly);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
if (unityPlayerResult == FindModuleResult.NoModuleFound && userAssemblyResult == FindModuleResult.NoModuleFound)
|
||||
{
|
||||
info = default;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
info = default;
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
|
||||
private async ValueTask<ValueResult<FindModuleResult, GameModule>> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)
|
||||
{
|
||||
ValueStopwatch watch = ValueStopwatch.StartNew();
|
||||
using (PeriodicTimer timer = new(findModuleDelay))
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
|
||||
{
|
||||
FindModuleResult result = UnsafeGetGameModuleInfo((HANDLE)gameProcess.Handle, out GameModule gameModule);
|
||||
if (result == FindModuleResult.Ok)
|
||||
{
|
||||
return new(FindModuleResult.Ok, gameModule);
|
||||
}
|
||||
|
||||
if (result == FindModuleResult.NoModuleFound)
|
||||
{
|
||||
return new(FindModuleResult.NoModuleFound, default);
|
||||
}
|
||||
|
||||
if (watch.GetElapsedTime() > findModuleLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new(FindModuleResult.TimeLimitExeeded, default);
|
||||
}
|
||||
|
||||
private async ValueTask LoopAdjustFpsAsync(TimeSpan adjustFpsDelay, IProgress<UnlockerStatus> progress, CancellationToken token)
|
||||
{
|
||||
using (PeriodicTimer timer = new(adjustFpsDelay))
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
if (!state.GameProcess.HasExited && state.FpsAddress != 0U)
|
||||
if (!gameProcess.HasExited && status.FpsAddress != 0U)
|
||||
{
|
||||
UnsafeWriteProcessMemory(state.GameProcess, state.FpsAddress, launchOptions.TargetFps);
|
||||
state.Report();
|
||||
UnsafeWriteProcessMemory(gameProcess, status.FpsAddress, launchOptions.TargetFps);
|
||||
progress.Report(status);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.IsUnlockerValid = false;
|
||||
state.FpsAddress = 0;
|
||||
state.Report();
|
||||
status.IsUnlockerValid = false;
|
||||
status.FpsAddress = 0;
|
||||
progress.Report(status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int value)
|
||||
private unsafe void UnsafeFindFpsAddress(in GameModule moduleEntryInfo)
|
||||
{
|
||||
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _);
|
||||
bool readOk = UnsafeReadModulesMemory(gameProcess, moduleEntryInfo, out VirtualMemory localMemory);
|
||||
Verify.Operation(readOk, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
|
||||
|
||||
using (localMemory)
|
||||
{
|
||||
int offset = IndexOfPattern(localMemory.AsSpan()[(int)moduleEntryInfo.UnityPlayer.Size..]);
|
||||
Must.Range(offset >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
|
||||
byte* pLocalMemory = (byte*)localMemory.Pointer;
|
||||
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
|
||||
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
|
||||
|
||||
nuint localMemoryUnityPlayerAddress = (nuint)pLocalMemory;
|
||||
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size;
|
||||
|
||||
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
|
||||
rip += 5U;
|
||||
rip += (nuint)(*(int*)(rip + 2U) + 6);
|
||||
|
||||
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
|
||||
|
||||
nuint ptr = 0;
|
||||
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
|
||||
|
||||
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
|
||||
|
||||
// CALL or JMP
|
||||
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
|
||||
{
|
||||
rip += (nuint)(*(int*)(rip + 1) + 5);
|
||||
}
|
||||
|
||||
nuint localMemoryActualAddress = rip + *(uint*)(rip + 2) + 6;
|
||||
nuint actualOffset = localMemoryActualAddress - localMemoryUnityPlayerAddress;
|
||||
status.FpsAddress = unityPlayer.Address + actualOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct GameModule
|
||||
{
|
||||
public readonly bool HasValue = false;
|
||||
public readonly Module UnityPlayer;
|
||||
public readonly Module 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
/// <summary>
|
||||
/// 解锁状态
|
||||
/// </summary>
|
||||
internal sealed class GameFpsUnlockerState
|
||||
{
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
public FindModuleResult FindModuleResult { get; set; }
|
||||
|
||||
public bool IsUnlockerValid { get; set; } = true;
|
||||
|
||||
public nuint FpsAddress { get; set; }
|
||||
|
||||
public UnlockTimingOptions TimingOptions { get; set; }
|
||||
|
||||
public Process GameProcess { get; set; } = default!;
|
||||
|
||||
public IProgress<GameFpsUnlockerState> Progress { get; set; } = default!;
|
||||
|
||||
public void Report()
|
||||
{
|
||||
Progress.Report(this);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.Memory;
|
||||
using Snap.Hutao.Win32.System.ProcessStatus;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
internal static class GameProcessModule
|
||||
{
|
||||
public static async ValueTask<ValueResult<FindModuleResult, RequiredGameModule>> FindModuleAsync(GameFpsUnlockerState state)
|
||||
{
|
||||
ValueStopwatch watch = ValueStopwatch.StartNew();
|
||||
using (PeriodicTimer timer = new(state.TimingOptions.FindModuleDelay))
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
|
||||
{
|
||||
FindModuleResult result = UnsafeGetGameModuleInfo((HANDLE)state.GameProcess.Handle, out RequiredGameModule gameModule);
|
||||
if (result == FindModuleResult.Ok)
|
||||
{
|
||||
return new(FindModuleResult.Ok, gameModule);
|
||||
}
|
||||
|
||||
if (result == FindModuleResult.NoModuleFound)
|
||||
{
|
||||
return new(FindModuleResult.NoModuleFound, default);
|
||||
}
|
||||
|
||||
if (watch.GetElapsedTime() > state.TimingOptions.FindModuleLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new(FindModuleResult.TimeLimitExeeded, default);
|
||||
}
|
||||
|
||||
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out RequiredGameModule info)
|
||||
{
|
||||
FindModuleResult unityPlayerResult = UnsafeFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
|
||||
FindModuleResult userAssemblyResult = UnsafeFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
|
||||
|
||||
if (unityPlayerResult is FindModuleResult.Ok && userAssemblyResult is FindModuleResult.Ok)
|
||||
{
|
||||
info = new(unityPlayer, userAssembly);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
if (unityPlayerResult is FindModuleResult.NoModuleFound && userAssemblyResult is FindModuleResult.NoModuleFound)
|
||||
{
|
||||
info = default;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
info = default;
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
|
||||
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, out Module module)
|
||||
{
|
||||
HMODULE[] buffer = new HMODULE[128];
|
||||
if (!K32EnumProcessModules(hProcess, buffer, out uint actualSize))
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
}
|
||||
|
||||
if (actualSize == 0)
|
||||
{
|
||||
module = default!;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
foreach (ref readonly HMODULE hModule in buffer.AsSpan()[..(int)(actualSize / sizeof(HMODULE))])
|
||||
{
|
||||
char[] baseName = new char[256];
|
||||
|
||||
if (K32GetModuleBaseNameW(hProcess, hModule, baseName) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fixed (char* lpBaseName = baseName)
|
||||
{
|
||||
if (!moduleName.SequenceEqual(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!K32GetModuleInformation(hProcess, hModule, out MODULEINFO moduleInfo))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
module = new((nuint)moduleInfo.lpBaseOfDll, moduleInfo.SizeOfImage);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
module = default;
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,12 @@ namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
[HighQuality]
|
||||
internal interface IGameFpsUnlocker
|
||||
{
|
||||
ValueTask PostUnlockAsync(CancellationToken token = default);
|
||||
|
||||
ValueTask UnlockAsync(CancellationToken token = default);
|
||||
/// <summary>
|
||||
/// 异步的解锁帧数限制
|
||||
/// </summary>
|
||||
/// <param name="options">选项</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>解锁的结果</returns>
|
||||
ValueTask UnlockAsync(UnlockTimingOptions options, IProgress<UnlockerStatus> progress, CancellationToken token = default);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
internal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
internal readonly struct RequiredGameModule
|
||||
{
|
||||
public readonly bool HasValue = false;
|
||||
public readonly Module UnityPlayer;
|
||||
public readonly Module UserAssembly;
|
||||
|
||||
public RequiredGameModule(in Module unityPlayer, in Module userAssembly)
|
||||
{
|
||||
HasValue = true;
|
||||
UnityPlayer = unityPlayer;
|
||||
UserAssembly = userAssembly;
|
||||
}
|
||||
}
|
||||
@@ -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 sealed class 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; }
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
d:DataContext="{d:DesignInstance shva:AchievementViewModelSlim}"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}"
|
||||
Command="{Binding NavigateCommand}"
|
||||
Style="{ThemeResource DefaultButtonStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
d:DataContext="{d:DesignInstance shvd:DailyNoteViewModelSlim}"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}"
|
||||
Command="{Binding NavigateCommand}"
|
||||
Style="{ThemeResource DefaultButtonStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
d:DataContext="{d:DesignInstance shvg:GachaLogViewModelSlim}"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}"
|
||||
Command="{Binding NavigateCommand}"
|
||||
Style="{ThemeResource DefaultButtonStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
VerticalContentAlignment="Stretch"
|
||||
d:DataContext="{d:DesignInstance shvg:LaunchGameViewModelSlim}"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}"
|
||||
Command="{Binding LaunchCommand}"
|
||||
Style="{ThemeResource DefaultButtonStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -21,12 +21,6 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<shvconv:BoolToGridLengthConverter x:Key="BoolToGridLengthConverter"/>
|
||||
<shvconv:BoolToGridLengthConverter
|
||||
x:Key="BoolToGridLengthSpacingConverter"
|
||||
FalseValue="0"
|
||||
TrueValue="4"/>
|
||||
|
||||
<shvconv:Int32ToGradientColorConverter x:Key="Int32ToGradientColorConverter" MaximumValue="{Binding GuaranteeOrangeThreshold}"/>
|
||||
|
||||
<DataTemplate x:Key="OrangeListTemplate" d:DataType="shvg:SummaryItem">
|
||||
@@ -232,12 +226,10 @@
|
||||
Padding="0,12"
|
||||
Value="{x:Bind StatisticsSegmented.SelectedIndex, Mode=OneWay}">
|
||||
<cwcont:Case Value="{shcm:Int32 Value=0}">
|
||||
<Grid>
|
||||
<Grid ColumnSpacing="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="{x:Bind ShowUpPull, Converter={StaticResource BoolToGridLengthSpacingConverter}}"/>
|
||||
<ColumnDefinition Width="{x:Bind ShowUpPull, Converter={StaticResource BoolToGridLengthConverter}}"/>
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" Style="{ThemeResource BorderCardStyle}">
|
||||
@@ -251,7 +243,7 @@
|
||||
</StackPanel>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
<Border Grid.Column="2" Style="{ThemeResource BorderCardStyle}">
|
||||
<Border Grid.Column="1" Style="{ThemeResource BorderCardStyle}">
|
||||
<Viewbox Margin="8,0" StretchDirection="DownOnly">
|
||||
<Grid>
|
||||
<StackPanel
|
||||
@@ -274,7 +266,7 @@
|
||||
</Viewbox>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="4" RowSpacing="4">
|
||||
<Grid Grid.Column="2" RowSpacing="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
@@ -306,7 +298,7 @@
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid ColumnSpacing="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
@@ -368,7 +360,7 @@
|
||||
|
||||
</cwcont:Case>
|
||||
<cwcont:Case Value="{shcm:Int32 Value=2}">
|
||||
<Grid RowSpacing="4">
|
||||
<Grid RowSpacing="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Converters;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.View.Converter;
|
||||
|
||||
internal sealed class BoolToGridLengthConverter : BoolToObjectConverter
|
||||
{
|
||||
public BoolToGridLengthConverter()
|
||||
{
|
||||
TrueValue = new GridLength(1D, GridUnitType.Star);
|
||||
FalseValue = new GridLength(0D);
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@
|
||||
LocalSettingKeySuffixForCurrent="AchievementPage.AchievementGoals"/>
|
||||
<Viewbox
|
||||
Height="32"
|
||||
MaxWidth="190"
|
||||
MaxWidth="192"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Stretch="Uniform"
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
cw:VisualExtensions.NormalizedCenterPoint="0.5">
|
||||
<cww:ConstrainedBox AspectRatio="1080:390" CornerRadius="{ThemeResource ControlCornerRadiusTop}">
|
||||
<shci:CachedImage
|
||||
Margin="-1"
|
||||
VerticalAlignment="Center"
|
||||
PlaceholderMargin="16"
|
||||
PlaceholderSource="{StaticResource UI_EmotionIcon271}"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:clw="using:CommunityToolkit.Labs.WinUI"
|
||||
xmlns:cw="using:CommunityToolkit.WinUI"
|
||||
xmlns:cwcont="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:cwconv="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
@@ -51,248 +50,240 @@
|
||||
<SplitView.Pane>
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="16" Spacing="3">
|
||||
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<cwcont:SettingsExpander
|
||||
Description="{Binding RuntimeOptions.Version}"
|
||||
Header="{shcm:ResourceString Name=AppName}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<cwcont:SettingsExpander.Items>
|
||||
<cwcont:SettingsCard
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCopyDeviceIdAction}"
|
||||
Command="{Binding CopyDeviceIdCommand}"
|
||||
Description="{Binding RuntimeOptions.DeviceId}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingDeviceIdHeader}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard Description="{Binding IPInformation}" Header="{shcm:ResourceString Name=ViewPageSettingDeviceIpHeader}"/>
|
||||
<cwcont:SettingsCard Description="{Binding DynamicHttpProxy.CurrentProxyUri}" Header="{shcm:ResourceString Name=ViewPageFeedbackCurrentProxyHeader}"/>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding EnableLoopbackCommand}"
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackEnableLoopbackHeader}"
|
||||
IsClickEnabled="True"
|
||||
IsEnabled="{Binding LoopbackManager.IsLoopbackEnabled, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
|
||||
<cwcont:SettingsCard.Description>
|
||||
<UserControl Content="{Binding LoopbackManager.IsLoopbackEnabled, Converter={StaticResource BoolToLoopbackDescriptionControlConverter}, Mode=OneWay}"/>
|
||||
</cwcont:SettingsCard.Description>
|
||||
</cwcont:SettingsCard>
|
||||
<cwcont:SettingsCard Description="{Binding RuntimeOptions.WebView2Version}" Header="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
|
||||
</cwcont:SettingsExpander.Items>
|
||||
</cwcont:SettingsExpander>
|
||||
</Border>
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<cwcont:SettingsExpander
|
||||
Description="{Binding RuntimeOptions.Version}"
|
||||
Header="{shcm:ResourceString Name=AppName}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<cwcont:SettingsExpander.Items>
|
||||
<cwcont:SettingsCard
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCopyDeviceIdAction}"
|
||||
Command="{Binding CopyDeviceIdCommand}"
|
||||
Description="{Binding RuntimeOptions.DeviceId}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingDeviceIdHeader}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard Description="{Binding IPInformation}" Header="{shcm:ResourceString Name=ViewPageSettingDeviceIpHeader}"/>
|
||||
<cwcont:SettingsCard Description="{Binding DynamicHttpProxy.CurrentProxyUri}" Header="{shcm:ResourceString Name=ViewPageFeedbackCurrentProxyHeader}"/>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding EnableLoopbackCommand}"
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackEnableLoopbackHeader}"
|
||||
IsClickEnabled="True"
|
||||
IsEnabled="{Binding LoopbackManager.IsLoopbackEnabled, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
|
||||
<cwcont:SettingsCard.Description>
|
||||
<UserControl Content="{Binding LoopbackManager.IsLoopbackEnabled, Converter={StaticResource BoolToLoopbackDescriptionControlConverter}, Mode=OneWay}"/>
|
||||
</cwcont:SettingsCard.Description>
|
||||
</cwcont:SettingsCard>
|
||||
<cwcont:SettingsCard Description="{Binding RuntimeOptions.WebView2Version}" Header="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
|
||||
</cwcont:SettingsExpander.Items>
|
||||
</cwcont:SettingsExpander>
|
||||
</Border>
|
||||
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<cwcont:SettingsExpander
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackEngageWithUsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackCommonLinksHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<cwcont:SettingsExpander.Items>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose"
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackGithubIssuesDescription}"
|
||||
Header="GitHub Issues"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://github.com/orgs/DGP-Studio/projects/2"
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackRoadmapDescription}"
|
||||
Header="GitHub Projects"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://status.hut.ao"
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackServerStatusDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackServerStatusHeader}"
|
||||
IsClickEnabled="True"/>
|
||||
</cwcont:SettingsExpander.Items>
|
||||
</cwcont:SettingsExpander>
|
||||
</Border>
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<cwcont:SettingsExpander
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackEngageWithUsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackCommonLinksHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<cwcont:SettingsExpander.Items>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose"
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackGithubIssuesDescription}"
|
||||
Header="GitHub Issues"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://github.com/orgs/DGP-Studio/projects/2"
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackRoadmapDescription}"
|
||||
Header="GitHub Projects"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://status.hut.ao"
|
||||
Description="{shcm:ResourceString Name=ViewPageFeedbackServerStatusDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackServerStatusHeader}"
|
||||
IsClickEnabled="True"/>
|
||||
</cwcont:SettingsExpander.Items>
|
||||
</cwcont:SettingsExpander>
|
||||
</Border>
|
||||
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<cwcont:SettingsExpander
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackFeatureGuideHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<cwcont:SettingsExpander.Items>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/dashboard.html"
|
||||
Header="{shcm:ResourceString Name=ViewAnnouncementHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Announcement.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/game-launcher.html"
|
||||
Header="{shcm:ResourceString Name=ViewLaunchGameHeader}"
|
||||
IsClickEnabled="True">
|
||||
<cwcont:SettingsCard.HeaderIcon>
|
||||
<!-- This icon is not a square -->
|
||||
<BitmapIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
ShowAsMonochrome="False"
|
||||
UriSource="ms-appx:///Resource/Navigation/LaunchGame.png"/>
|
||||
</cwcont:SettingsCard.HeaderIcon>
|
||||
</cwcont:SettingsCard>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/wish-export.html"
|
||||
Header="{shcm:ResourceString Name=ViewGachaLogHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/GachaLog.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/achievements.html"
|
||||
Header="{shcm:ResourceString Name=ViewAchievementHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Achievement.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/real-time-notes.html"
|
||||
Header="{shcm:ResourceString Name=ViewDailyNoteHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/DailyNote.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/character-data.html"
|
||||
Header="{shcm:ResourceString Name=ViewAvatarPropertyHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/AvatarProperty.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/hutao-API.html"
|
||||
Header="{shcm:ResourceString Name=ViewSpiralAbyssHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/SpiralAbyss.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/develop-plan.html"
|
||||
Header="{shcm:ResourceString Name=ViewCultivationHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Cultivation.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/character-wiki.html"
|
||||
Header="{shcm:ResourceString Name=ViewWikiAvatarHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiAvatar.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/weapon-wiki.html"
|
||||
Header="{shcm:ResourceString Name=ViewWikiWeaponHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiWeapon.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/monster-wiki.html"
|
||||
Header="{shcm:ResourceString Name=ViewWikiMonsterHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiMonster.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/hutao-settings.html"
|
||||
Header="{shcm:ResourceString Name=ViewSettingHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"/>
|
||||
</cwcont:SettingsExpander.Items>
|
||||
</cwcont:SettingsExpander>
|
||||
</Border>
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<cwcont:SettingsExpander
|
||||
Header="{shcm:ResourceString Name=ViewPageFeedbackFeatureGuideHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<cwcont:SettingsExpander.Items>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/dashboard.html"
|
||||
Header="{shcm:ResourceString Name=ViewAnnouncementHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Announcement.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/game-launcher.html"
|
||||
Header="{shcm:ResourceString Name=ViewLaunchGameHeader}"
|
||||
IsClickEnabled="True">
|
||||
<cwcont:SettingsCard.HeaderIcon>
|
||||
<!-- This icon is not a square -->
|
||||
<BitmapIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
ShowAsMonochrome="False"
|
||||
UriSource="ms-appx:///Resource/Navigation/LaunchGame.png"/>
|
||||
</cwcont:SettingsCard.HeaderIcon>
|
||||
</cwcont:SettingsCard>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/wish-export.html"
|
||||
Header="{shcm:ResourceString Name=ViewGachaLogHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/GachaLog.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/achievements.html"
|
||||
Header="{shcm:ResourceString Name=ViewAchievementHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Achievement.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/real-time-notes.html"
|
||||
Header="{shcm:ResourceString Name=ViewDailyNoteHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/DailyNote.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/character-data.html"
|
||||
Header="{shcm:ResourceString Name=ViewAvatarPropertyHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/AvatarProperty.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/hutao-API.html"
|
||||
Header="{shcm:ResourceString Name=ViewSpiralAbyssHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/SpiralAbyss.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/develop-plan.html"
|
||||
Header="{shcm:ResourceString Name=ViewCultivationHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Cultivation.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/character-wiki.html"
|
||||
Header="{shcm:ResourceString Name=ViewWikiAvatarHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiAvatar.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/weapon-wiki.html"
|
||||
Header="{shcm:ResourceString Name=ViewWikiWeaponHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiWeapon.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/monster-wiki.html"
|
||||
Header="{shcm:ResourceString Name=ViewWikiMonsterHeader}"
|
||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiMonster.png}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwcont:SettingsCard
|
||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
||||
Command="{Binding NavigateToUriCommand}"
|
||||
CommandParameter="https://hut.ao/features/hutao-settings.html"
|
||||
Header="{shcm:ResourceString Name=ViewSettingHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"/>
|
||||
</cwcont:SettingsExpander.Items>
|
||||
</cwcont:SettingsExpander>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</SplitView.Pane>
|
||||
<Border Margin="16,16,0,16" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
<Grid Style="{ThemeResource AcrylicGridCardStyle}">
|
||||
<Grid
|
||||
Padding="16"
|
||||
RowSpacing="8"
|
||||
Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<AutoSuggestBox
|
||||
Grid.Row="0"
|
||||
Height="36"
|
||||
Margin="0,0,0,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewPageFeedbackAutoSuggestBoxPlaceholder}"
|
||||
QueryIcon="{shcm:FontIcon Glyph=}"
|
||||
Style="{StaticResource DefaultAutoSuggestBoxStyle}"
|
||||
Text="{Binding SearchText, Mode=TwoWay}">
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="QuerySubmitted">
|
||||
<mxic:InvokeCommandAction Command="{Binding SearchDocumentCommand}" CommandParameter="{Binding SearchText}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding SearchResults.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}">
|
||||
<shci:CachedImage
|
||||
Height="120"
|
||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
||||
EnableLazyLoading="False"
|
||||
Source="{StaticResource UI_EmotionIcon52}"/>
|
||||
<TextBlock
|
||||
Margin="0,5,0,21"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewPageFeedbackSearchResultPlaceholderTitle}"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
|
||||
<ItemsControl
|
||||
ItemContainerTransitions="{ThemeResource ListViewLikeThemeTransitions}"
|
||||
ItemsPanel="{ThemeResource StackPanelSpacing8Template}"
|
||||
ItemsSource="{Binding SearchResults}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Style="{ThemeResource BorderCardStyle}">
|
||||
<HyperlinkButton
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
NavigateUri="{Binding Url}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<BreadcrumbBar
|
||||
Grid.Column="0"
|
||||
Margin="4,8,8,4"
|
||||
IsHitTestVisible="False"
|
||||
ItemsSource="{Binding Hierarchy.DisplayLevels}"/>
|
||||
</Grid>
|
||||
</HyperlinkButton>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<clw:Shimmer IsActive="{Binding IsInitialized, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityRevertConverter}, Mode=OneWay}"/>
|
||||
<Grid Margin="16,16,0,16" Style="{ThemeResource AcrylicGridCardStyle}">
|
||||
<Grid
|
||||
Padding="16"
|
||||
RowSpacing="8"
|
||||
Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<AutoSuggestBox
|
||||
Grid.Row="0"
|
||||
Height="36"
|
||||
Margin="0,0,0,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewPageFeedbackAutoSuggestBoxPlaceholder}"
|
||||
QueryIcon="{shcm:FontIcon Glyph=}"
|
||||
Style="{StaticResource DefaultAutoSuggestBoxStyle}"
|
||||
Text="{Binding SearchText, Mode=TwoWay}">
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="QuerySubmitted">
|
||||
<mxic:InvokeCommandAction Command="{Binding SearchDocumentCommand}" CommandParameter="{Binding SearchText}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding SearchResults.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}">
|
||||
<shci:CachedImage
|
||||
Height="120"
|
||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
||||
EnableLazyLoading="False"
|
||||
Source="{StaticResource UI_EmotionIcon52}"/>
|
||||
<TextBlock
|
||||
Margin="0,5,0,21"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewPageFeedbackSearchResultPlaceholderTitle}"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
|
||||
<ItemsControl
|
||||
ItemContainerTransitions="{ThemeResource ListViewLikeThemeTransitions}"
|
||||
ItemsPanel="{ThemeResource StackPanelSpacing8Template}"
|
||||
ItemsSource="{Binding SearchResults}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Style="{ThemeResource BorderCardStyle}">
|
||||
<HyperlinkButton
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
NavigateUri="{Binding Url}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<BreadcrumbBar
|
||||
Grid.Column="0"
|
||||
Margin="4,8,8,4"
|
||||
IsHitTestVisible="False"
|
||||
ItemsSource="{Binding Hierarchy.DisplayLevels}"/>
|
||||
</Grid>
|
||||
</HyperlinkButton>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<clw:Shimmer IsActive="{Binding IsInitialized, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityRevertConverter}, Mode=OneWay}"/>
|
||||
</Grid>
|
||||
</SplitView>
|
||||
</Grid>
|
||||
</shc:ScopedPage>
|
||||
@@ -12,7 +12,6 @@
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
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:shvg="using:Snap.Hutao.ViewModel.GachaLog"
|
||||
d:DataContext="{d:DesignInstance shvg:GachaLogViewModel}"
|
||||
@@ -143,8 +142,8 @@
|
||||
<Border Width="40" Style="{StaticResource BorderCardStyle}">
|
||||
<StackPanel>
|
||||
<shvc:ItemIcon
|
||||
Width="38"
|
||||
Height="38"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Icon="{Binding Icon}"
|
||||
Quality="{Binding Quality}"/>
|
||||
<TextBlock
|
||||
@@ -183,19 +182,17 @@
|
||||
Text="{Binding TotalCountFormatted}"/>
|
||||
<ItemsControl
|
||||
Grid.Row="1"
|
||||
MaxWidth="124"
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemsPanel="{StaticResource WrapPanelSpacing2Template}"
|
||||
ItemsPanel="{StaticResource HorizontalStackPanelSpacing2Template}"
|
||||
ItemsSource="{Binding OrangeUpList}"/>
|
||||
<ItemsControl
|
||||
Grid.Row="1"
|
||||
MaxWidth="252"
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemsPanel="{StaticResource WrapPanelSpacing2Template}"
|
||||
ItemsPanel="{StaticResource HorizontalStackPanelSpacing2Template}"
|
||||
ItemsSource="{Binding PurpleUpList}"/>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
@@ -288,21 +285,26 @@
|
||||
</CommandBar>
|
||||
</Pivot.RightHeader>
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageGahcaLogPivotOverview}">
|
||||
<ScrollViewer
|
||||
Margin="16,0"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
HorizontalScrollMode="Enabled"
|
||||
VerticalScrollMode="Disabled">
|
||||
<shcp:HorizontalEqualPanel
|
||||
Margin="0,16"
|
||||
MinItemWidth="302"
|
||||
Spacing="16">
|
||||
<shvc:StatisticsCard DataContext="{Binding Statistics.AvatarWish}"/>
|
||||
<shvc:StatisticsCard DataContext="{Binding Statistics.WeaponWish}"/>
|
||||
<shvc:StatisticsCard DataContext="{Binding Statistics.StandardWish}" ShowUpPull="False"/>
|
||||
<shvc:StatisticsCard DataContext="{Binding Statistics.ChronicledWish}" ShowUpPull="False"/>
|
||||
</shcp:HorizontalEqualPanel>
|
||||
</ScrollViewer>
|
||||
<Grid Margin="0,0,16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shvc:StatisticsCard
|
||||
Grid.Column="0"
|
||||
Margin="16,16,0,16"
|
||||
DataContext="{Binding Statistics.AvatarWish}"/>
|
||||
<shvc:StatisticsCard
|
||||
Grid.Column="1"
|
||||
Margin="16,16,0,16"
|
||||
DataContext="{Binding Statistics.WeaponWish}"/>
|
||||
<shvc:StatisticsCard
|
||||
Grid.Column="2"
|
||||
Margin="16,16,0,16"
|
||||
DataContext="{Binding Statistics.StandardWish}"
|
||||
ShowUpPull="False"/>
|
||||
</Grid>
|
||||
</PivotItem>
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageGahcaLogPivotHistory}">
|
||||
<Border Margin="16" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
@@ -310,7 +312,7 @@
|
||||
<SplitView
|
||||
DisplayMode="Inline"
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="408"
|
||||
OpenPaneLength="323"
|
||||
PaneBackground="{ThemeResource CardBackgroundFillColorSecondaryBrush}">
|
||||
<SplitView.Pane>
|
||||
<ListView
|
||||
@@ -377,6 +379,7 @@
|
||||
</SplitView>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
</PivotItem>
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageGahcaLogPivotAvatar}">
|
||||
<ScrollViewer>
|
||||
@@ -484,6 +487,7 @@
|
||||
<shvc:HutaoStatisticsCard Grid.Column="2" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.WeaponEvent}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</Grid>
|
||||
|
||||
@@ -348,7 +348,6 @@
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Header="{shcm:ResourceString Name=ViewPageSettingBackgroundImageLocalFolderCopyrightHeader}" Visibility="{Binding BackgroundImageOptions.Wallpaper, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
</StackPanel>
|
||||
|
||||
@@ -203,23 +203,19 @@
|
||||
</Border>
|
||||
</cwc:Case>
|
||||
<cwc:Case Value="Grid">
|
||||
<Border Margin="16,0,16,16" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
<Border Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<GridView
|
||||
Padding="16,16,4,4"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
|
||||
ItemTemplate="{StaticResource MonsterGridTemplate}"
|
||||
ItemsSource="{Binding Monsters}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
SelectionMode="Single">
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:SelectedItemInViewBehavior/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</GridView>
|
||||
</Border>
|
||||
</Border>
|
||||
<GridView
|
||||
Padding="16,16,4,4"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
|
||||
ItemTemplate="{StaticResource MonsterGridTemplate}"
|
||||
ItemsSource="{Binding Monsters}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
SelectionMode="Single">
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:SelectedItemInViewBehavior/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</GridView>
|
||||
</cwc:Case>
|
||||
</cwc:SwitchPresenter>
|
||||
</Grid>
|
||||
|
||||
@@ -104,14 +104,10 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
|
||||
ArgumentNullException.ThrowIfNull(gachaLogService.ArchiveCollection);
|
||||
ObservableCollection<GachaArchive> archives = gachaLogService.ArchiveCollection;
|
||||
|
||||
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Archives = archives;
|
||||
HutaoCloudViewModel.RetrieveCommand = RetrieveFromCloudCommand;
|
||||
await SetSelectedArchiveAndUpdateStatisticsAsync(Archives.SelectedOrDefault(), true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Archives = archives;
|
||||
HutaoCloudViewModel.RetrieveCommand = RetrieveFromCloudCommand;
|
||||
await SetSelectedArchiveAndUpdateStatisticsAsync(Archives.SelectedOrDefault(), true).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,49 +92,39 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
|
||||
protected override async ValueTask<bool> InitializeUIAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
if (!await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
levelAvatarCurveMap = await metadataService.GetLevelToAvatarCurveMapAsync().ConfigureAwait(false);
|
||||
promotes = await metadataService.GetAvatarPromoteListAsync().ConfigureAwait(false);
|
||||
|
||||
Dictionary<MaterialId, Material> idMaterialMap = await metadataService.GetIdToMaterialMapAsync().ConfigureAwait(false);
|
||||
List<Avatar> avatars = await metadataService.GetAvatarListAsync().ConfigureAwait(false);
|
||||
IOrderedEnumerable<Avatar> sorted = avatars
|
||||
.OrderByDescending(avatar => avatar.BeginTime)
|
||||
.ThenByDescending(avatar => avatar.Sort);
|
||||
List<Avatar> list = [.. sorted];
|
||||
|
||||
await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false);
|
||||
|
||||
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Avatars = new(list, true);
|
||||
Selected = Avatars.View.ElementAtOrDefault(0);
|
||||
}
|
||||
|
||||
FilterTokens = [];
|
||||
|
||||
availableTokens = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
.. avatars.Select(avatar => KeyValuePair.Create(avatar.Name, new SearchToken(SearchTokenKind.Avatar, avatar.Name, sideIconUri: AvatarSideIconConverter.IconNameToUri(avatar.SideIcon)))),
|
||||
.. IntrinsicFrozen.AssociationTypes.Select(assoc => KeyValuePair.Create(assoc, new SearchToken(SearchTokenKind.AssociationType, assoc, iconUri: AssociationTypeIconConverter.AssociationTypeNameToIconUri(assoc)))),
|
||||
.. IntrinsicFrozen.BodyTypes.Select(b => KeyValuePair.Create(b, new SearchToken(SearchTokenKind.BodyType, b))),
|
||||
.. IntrinsicFrozen.ElementNames.Select(e => KeyValuePair.Create(e, new SearchToken(SearchTokenKind.ElementName, e, iconUri: ElementNameIconConverter.ElementNameToIconUri(e)))),
|
||||
.. IntrinsicFrozen.ItemQualities.Select(i => KeyValuePair.Create(i, new SearchToken(SearchTokenKind.ItemQuality, i, quality: QualityColorConverter.QualityNameToColor(i)))),
|
||||
.. IntrinsicFrozen.WeaponTypes.Select(w => KeyValuePair.Create(w, new SearchToken(SearchTokenKind.WeaponType, w, iconUri: WeaponTypeIconConverter.WeaponTypeNameToIconUri(w)))),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
levelAvatarCurveMap = await metadataService.GetLevelToAvatarCurveMapAsync().ConfigureAwait(false);
|
||||
promotes = await metadataService.GetAvatarPromoteListAsync().ConfigureAwait(false);
|
||||
|
||||
Dictionary<MaterialId, Material> idMaterialMap = await metadataService.GetIdToMaterialMapAsync().ConfigureAwait(false);
|
||||
List<Avatar> avatars = await metadataService.GetAvatarListAsync().ConfigureAwait(false);
|
||||
IOrderedEnumerable<Avatar> sorted = avatars
|
||||
.OrderByDescending(avatar => avatar.BeginTime)
|
||||
.ThenByDescending(avatar => avatar.Sort);
|
||||
List<Avatar> list = [.. sorted];
|
||||
|
||||
await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false);
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Avatars = new(list, true);
|
||||
Selected = Avatars.View.ElementAtOrDefault(0);
|
||||
FilterTokens = [];
|
||||
|
||||
availableTokens = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
.. avatars.Select(avatar => KeyValuePair.Create(avatar.Name, new SearchToken(SearchTokenKind.Avatar, avatar.Name, sideIconUri: AvatarSideIconConverter.IconNameToUri(avatar.SideIcon)))),
|
||||
.. IntrinsicFrozen.AssociationTypes.Select(assoc => KeyValuePair.Create(assoc, new SearchToken(SearchTokenKind.AssociationType, assoc, iconUri: AssociationTypeIconConverter.AssociationTypeNameToIconUri(assoc)))),
|
||||
.. IntrinsicFrozen.BodyTypes.Select(b => KeyValuePair.Create(b, new SearchToken(SearchTokenKind.BodyType, b))),
|
||||
.. IntrinsicFrozen.ElementNames.Select(e => KeyValuePair.Create(e, new SearchToken(SearchTokenKind.ElementName, e, iconUri: ElementNameIconConverter.ElementNameToIconUri(e)))),
|
||||
.. IntrinsicFrozen.ItemQualities.Select(i => KeyValuePair.Create(i, new SearchToken(SearchTokenKind.ItemQuality, i, quality: QualityColorConverter.QualityNameToColor(i)))),
|
||||
.. IntrinsicFrozen.WeaponTypes.Select(w => KeyValuePair.Create(w, new SearchToken(SearchTokenKind.WeaponType, w, iconUri: WeaponTypeIconConverter.WeaponTypeNameToIconUri(w)))),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask CombineComplexDataAsync(List<Avatar> avatars, Dictionary<MaterialId, Material> idMaterialMap)
|
||||
|
||||
@@ -53,31 +53,21 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
levelMonsterCurveMap = await metadataService.GetLevelToMonsterCurveMapAsync().ConfigureAwait(false);
|
||||
|
||||
List<Monster> monsters = await metadataService.GetMonsterListAsync().ConfigureAwait(false);
|
||||
Dictionary<MaterialId, DisplayItem> idDisplayMap = await metadataService.GetIdToDisplayItemAndMaterialMapAsync().ConfigureAwait(false);
|
||||
foreach (Monster monster in monsters)
|
||||
{
|
||||
monster.DropsView ??= monster.Drops?.SelectList(i => idDisplayMap.GetValueOrDefault(i, Material.Default));
|
||||
}
|
||||
|
||||
List<Monster> ordered = monsters.SortBy(m => m.RelationshipId.Value);
|
||||
|
||||
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Monsters = new(ordered, true);
|
||||
Selected = Monsters.View.ElementAtOrDefault(0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
levelMonsterCurveMap = await metadataService.GetLevelToMonsterCurveMapAsync().ConfigureAwait(false);
|
||||
|
||||
List<Monster> monsters = await metadataService.GetMonsterListAsync().ConfigureAwait(false);
|
||||
Dictionary<MaterialId, DisplayItem> idDisplayMap = await metadataService.GetIdToDisplayItemAndMaterialMapAsync().ConfigureAwait(false);
|
||||
foreach (Monster monster in monsters)
|
||||
{
|
||||
monster.DropsView ??= monster.Drops?.SelectList(i => idDisplayMap.GetValueOrDefault(i, Material.Default));
|
||||
}
|
||||
|
||||
List<Monster> ordered = monsters.SortBy(m => m.RelationshipId.Value);
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
Monsters = new(ordered, true);
|
||||
Selected = Monsters.View.ElementAtOrDefault(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -92,42 +92,32 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
levelWeaponCurveMap = await metadataService.GetLevelToWeaponCurveMapAsync().ConfigureAwait(false);
|
||||
promotes = await metadataService.GetWeaponPromoteListAsync().ConfigureAwait(false);
|
||||
Dictionary<MaterialId, Material> idMaterialMap = await metadataService.GetIdToMaterialMapAsync().ConfigureAwait(false);
|
||||
levelWeaponCurveMap = await metadataService.GetLevelToWeaponCurveMapAsync().ConfigureAwait(false);
|
||||
promotes = await metadataService.GetWeaponPromoteListAsync().ConfigureAwait(false);
|
||||
Dictionary<MaterialId, Material> idMaterialMap = await metadataService.GetIdToMaterialMapAsync().ConfigureAwait(false);
|
||||
|
||||
List<Weapon> weapons = await metadataService.GetWeaponListAsync().ConfigureAwait(false);
|
||||
IEnumerable<Weapon> sorted = weapons
|
||||
.OrderByDescending(weapon => weapon.RankLevel)
|
||||
.ThenBy(weapon => weapon.WeaponType)
|
||||
.ThenByDescending(weapon => weapon.Id.Value);
|
||||
List<Weapon> list = [.. sorted];
|
||||
List<Weapon> weapons = await metadataService.GetWeaponListAsync().ConfigureAwait(false);
|
||||
IEnumerable<Weapon> sorted = weapons
|
||||
.OrderByDescending(weapon => weapon.RankLevel)
|
||||
.ThenBy(weapon => weapon.WeaponType)
|
||||
.ThenByDescending(weapon => weapon.Id.Value);
|
||||
List<Weapon> list = [.. sorted];
|
||||
|
||||
await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false);
|
||||
await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false);
|
||||
|
||||
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
Weapons = new(list, true);
|
||||
Selected = Weapons.View.ElementAtOrDefault(0);
|
||||
}
|
||||
Weapons = new(list, true);
|
||||
Selected = Weapons.View.ElementAtOrDefault(0);
|
||||
FilterTokens = [];
|
||||
|
||||
FilterTokens = [];
|
||||
|
||||
availableTokens = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
.. weapons.Select(w => KeyValuePair.Create(w.Name, new SearchToken(SearchTokenKind.Weapon, w.Name, sideIconUri: EquipIconConverter.IconNameToUri(w.Icon)))),
|
||||
.. IntrinsicFrozen.FightProperties.Select(f => KeyValuePair.Create(f, new SearchToken(SearchTokenKind.FightProperty, f))),
|
||||
.. IntrinsicFrozen.ItemQualities.Select(i => KeyValuePair.Create(i, new SearchToken(SearchTokenKind.ItemQuality, i, quality: QualityColorConverter.QualityNameToColor(i)))),
|
||||
.. IntrinsicFrozen.WeaponTypes.Select(w => KeyValuePair.Create(w, new SearchToken(SearchTokenKind.WeaponType, w, iconUri: WeaponTypeIconConverter.WeaponTypeNameToIconUri(w)))),
|
||||
]);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
availableTokens = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
.. weapons.Select(w => KeyValuePair.Create(w.Name, new SearchToken(SearchTokenKind.Weapon, w.Name, sideIconUri: EquipIconConverter.IconNameToUri(w.Icon)))),
|
||||
.. IntrinsicFrozen.FightProperties.Select(f => KeyValuePair.Create(f, new SearchToken(SearchTokenKind.FightProperty, f))),
|
||||
.. IntrinsicFrozen.ItemQualities.Select(i => KeyValuePair.Create(i, new SearchToken(SearchTokenKind.ItemQuality, i, quality: QualityColorConverter.QualityNameToColor(i)))),
|
||||
.. IntrinsicFrozen.WeaponTypes.Select(w => KeyValuePair.Create(w, new SearchToken(SearchTokenKind.WeaponType, w, iconUri: WeaponTypeIconConverter.WeaponTypeNameToIconUri(w)))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
|
||||
|
||||
internal static class LatestPackageExtension
|
||||
{
|
||||
public static void Patch(this LatestPackage latest)
|
||||
{
|
||||
StringBuilder pathBuilder = new();
|
||||
foreach (PackageSegment segment in latest.Segments)
|
||||
{
|
||||
pathBuilder.AppendLine(segment.Path);
|
||||
}
|
||||
|
||||
latest.Path = pathBuilder.ToStringTrimEndReturn();
|
||||
latest.Name = latest.Segments[0].Path[..^4]; // .00X
|
||||
}
|
||||
}
|
||||
@@ -46,16 +46,18 @@ internal sealed partial class ResourceClient
|
||||
.TryCatchSendAsync<Response<GameResource>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// 最新版完整包
|
||||
// 补全缺失的信息
|
||||
if (resp is { Data.Game.Latest: LatestPackage latest })
|
||||
{
|
||||
latest.Patch();
|
||||
}
|
||||
StringBuilder pathBuilder = new();
|
||||
foreach (PackageSegment segment in latest.Segments)
|
||||
{
|
||||
pathBuilder.AppendLine(segment.Path);
|
||||
}
|
||||
|
||||
// 预下载完整包
|
||||
if (resp is { Data.PreDownloadGame.Latest: LatestPackage preDownloadLatest })
|
||||
{
|
||||
preDownloadLatest.Patch();
|
||||
latest.Path = pathBuilder.ToStringTrimEndReturn();
|
||||
string path = latest.Segments[0].Path[..^4]; // .00X
|
||||
latest.Name = Path.GetFileName(path);
|
||||
}
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
|
||||
Reference in New Issue
Block a user