Merge pull request #1182 from DGP-Studio/develop

This commit is contained in:
DismissedLight
2023-12-16 15:00:09 +08:00
committed by GitHub
59 changed files with 1126 additions and 781 deletions

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

View File

@@ -20,7 +20,24 @@ namespace Snap.Hutao;
[SuppressMessage("", "SH001")]
public sealed partial class App : Application
{
private const string ConsoleBanner = """
----------------------------------------------------------------
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
Snap.Hutao is a open source software developed by DGP Studio.
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
----------------------------------------------------------------
""";
private const string AppInstanceKey = "main";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly ILogger<App> logger;
@@ -51,6 +68,8 @@ public sealed partial class App : Application
if (firstInstance.IsCurrent)
{
logger.LogInformation(ConsoleBanner);
// manually invoke
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
activation.InitializeWith(firstInstance);

View File

@@ -2,6 +2,22 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.14"
Offset="0,4,0"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.28"
Offset="0,4,0"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style x:Key="BorderCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
@@ -14,8 +30,4 @@
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
Opacity="0.1"
Offset="0,4,0"/>
</ResourceDictionary>

View File

@@ -10,25 +10,25 @@
<x:String x:Key="Sponsor_Afadian">https://afdian.net/a/DismissedLight</x:String>
<!-- AvatarCard -->
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://api.snapgenshin.com/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<!-- Bg -->
<x:String x:Key="UI_Icon_Intee_Explore_1">https://static.snapgenshin.com/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://static.snapgenshin.com/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://static.snapgenshin.com/Bg/UI_MarkTower.png</x:String>
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<x:String x:Key="UI_ItemIcon_201">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_201.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_220021.png</x:String>
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
<!-- EmotionIcon -->
<x:String x:Key="UI_EmotionIcon25">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
</ResourceDictionary>

View File

@@ -3,18 +3,12 @@
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 指示该类可以解构为元组
/// </summary>
/// <typeparam name="T1">元组的第一个类型</typeparam>
/// <typeparam name="T2">元组的第二个类型</typeparam>
[HighQuality]
internal interface IDeconstruct<T1, T2>
{
/// <summary>
/// 解构
/// </summary>
/// <param name="t1">第一个元素</param>
/// <param name="t2">第二个元素</param>
void Deconstruct(out T1 t1, out T2 t2);
}
internal interface IDeconstruct<T1, T2, T3>
{
void Deconstruct(out T1 t1, out T2 t2, out T3 t3);
}

View File

@@ -24,7 +24,7 @@ internal static class DependencyInjection
ServiceProvider serviceProvider = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddConsoleWindow())
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
.AddMemoryCache()
// Hutao extensions

View File

@@ -23,13 +23,6 @@ internal static partial class EnumerableExtension
return true;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void IncreaseOne<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key)
where TKey : notnull
where TValue : struct, IIncrementOperators<TValue>
@@ -37,14 +30,6 @@ internal static partial class EnumerableExtension
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <param name="value">增加的值</param>
public static void IncreaseValue<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key, TValue value)
where TKey : notnull
where TValue : struct, IAdditionOperators<TValue, TValue, TValue>
@@ -54,14 +39,6 @@ internal static partial class EnumerableExtension
current += value;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncreaseOne<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key)
where TKey : notnull
where TValue : struct, IIncrementOperators<TValue>
@@ -76,7 +53,6 @@ internal static partial class EnumerableExtension
return false;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryIgnoringDuplicateKeys<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull
{
@@ -90,7 +66,6 @@ internal static partial class EnumerableExtension
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryIgnoringDuplicateKeys<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> elementSelector)
where TKey : notnull
{

View File

@@ -3,6 +3,7 @@
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Text;
namespace Snap.Hutao.Extension;
@@ -43,6 +44,63 @@ internal static partial class EnumerableExtension
return first;
}
public static string JoinToString<T>(this IEnumerable<T> source, char separator, Action<StringBuilder, T> selector)
{
StringBuilder resultBuilder = new();
IEnumerator<T> enumerator = source.GetEnumerator();
if (!enumerator.MoveNext())
{
return string.Empty;
}
T first = enumerator.Current;
selector(resultBuilder, first);
if (!enumerator.MoveNext())
{
return resultBuilder.ToString();
}
do
{
resultBuilder.Append(separator);
selector(resultBuilder, enumerator.Current);
}
while (enumerator.MoveNext());
return resultBuilder.ToString();
}
public static string JoinToString<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source, char separator, Action<StringBuilder, TKey, TValue> selector)
{
StringBuilder resultBuilder = new();
IEnumerator<KeyValuePair<TKey, TValue>> enumerator = source.GetEnumerator();
if (!enumerator.MoveNext())
{
return string.Empty;
}
KeyValuePair<TKey, TValue> first = enumerator.Current;
selector(resultBuilder, first.Key, first.Value);
if (!enumerator.MoveNext())
{
return resultBuilder.ToString();
}
do
{
resultBuilder.Append(separator);
KeyValuePair<TKey, TValue> current = enumerator.Current;
selector(resultBuilder, current.Key, current.Value);
}
while (enumerator.MoveNext());
return resultBuilder.ToString();
}
/// <summary>
/// 转换到 <see cref="ObservableCollection{T}"/>
/// </summary>
@@ -64,7 +122,6 @@ internal static partial class EnumerableExtension
/// <returns>Converted collection into string.</returns>
public static string ToString<T>(this IEnumerable<T> collection, char separator)
{
string result = string.Join(separator, collection);
return result.Length > 0 ? result : string.Empty;
return string.Join(separator, collection);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Entity.Extension;
internal static class UserExtension
{
public static bool TryUpdateFingerprint(this User user, string? deviceFp)
{
if (string.IsNullOrEmpty(deviceFp))
{
return false;
}
user.Fingerprint = deviceFp;
user.FingerprintLastUpdateTime = DateTimeOffset.UtcNow;
return true;
}
}

View File

@@ -12,7 +12,7 @@ internal enum SchemeType
/// <summary>
/// 国际服
/// </summary>
Mihoyo,
Hoyoverse,
/// <summary>
/// 国服官服

View File

@@ -13,6 +13,8 @@ internal sealed partial class SettingEntry
/// </summary>
public const string GamePath = "GamePath";
public const string GamePathEntries = "GamePathEntries";
/// <summary>
/// PowerShell 路径
/// </summary>
@@ -104,6 +106,8 @@ internal sealed partial class SettingEntry
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";

View File

@@ -2045,6 +2045,9 @@
<data name="ViewPageLaunchGameAppearanceBorderlessHeader" xml:space="preserve">
<value>无边框</value>
</data>
<data name="ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription" xml:space="preserve">
<value>启用内置触摸布局,不会响应键鼠输入</value>
</data>
<data name="ViewPageLaunchGameAppearanceExclusiveDescription" xml:space="preserve">
<value>与游戏内浏览器不兼容,切屏等操作也能使游戏闪退</value>
</data>
@@ -2189,6 +2192,9 @@
<data name="ViewPageOpenScreenshotFolderAction" xml:space="preserve">
<value>打开截图文件夹</value>
</data>
<data name="ViewPageResetGamePathAction" xml:space="preserve">
<value>选择游戏路径</value>
</data>
<data name="ViewPageSettingAboutHeader" xml:space="preserve">
<value>关于 胡桃</value>
</data>

View File

@@ -15,20 +15,12 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private string? gamePath;
private string? powerShellPath;
private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType;
private CultureInfo? currentCulture;
private bool? isAdvancedLaunchOptionsEnabled;
private string? geetestCustomCompositeUrl;
public string GamePath
{
get => GetOption(ref gamePath, SettingEntry.GamePath);
set => SetOption(ref gamePath, SettingEntry.GamePath, value);
}
public string PowerShellPath
{
get
@@ -80,14 +72,6 @@ internal sealed partial class AppOptions : DbStoreOptions
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
}
public bool IsAdvancedLaunchOptionsEnabled
{
// DO NOT MOVE TO OTHER CLASS
// We use this property in SettingPage binding
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
}
public string GeetestCustomCompositeUrl
{
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);

View File

@@ -3,45 +3,11 @@
using Snap.Hutao.Model;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service;
internal static class AppOptionsExtension
{
public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = appOptions.GamePath;
gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameFolder))
{
gameFileName = default;
return false;
}
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
return true;
}
public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
{
gamePath = appOptions.GamePath;
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
return true;
}
public static NameValue<CultureInfo>? GetCurrentCultureForSelectionOrDefault(this AppOptions appOptions)
{
return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture);

View File

@@ -41,8 +41,7 @@ internal static class RegistryInterop
string base64 = Convert.ToBase64String(target);
string path = $"HKCU:{GenshinPath}";
string command = $"""
$value = [Convert]::FromBase64String('{base64}');
Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;
-Command "$value = [Convert]::FromBase64String('{base64}'); Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;"
""";
ProcessStartInfo startInfo = new()

View File

@@ -13,11 +13,11 @@ namespace Snap.Hutao.Service.Game.Configuration;
[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))]
internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService
{
private readonly AppOptions appOptions;
private readonly LaunchOptions launchOptions;
public ChannelOptions GetChannelOptions()
{
string gamePath = appOptions.GamePath;
string gamePath = launchOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
@@ -38,7 +38,7 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public bool SetChannelOptions(LaunchScheme scheme)
{
string gamePath = appOptions.GamePath;
string gamePath = launchOptions.GamePath;
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Game.Process;
using Snap.Hutao.Service.Game.Scheme;
using System.Collections.ObjectModel;

View File

@@ -6,6 +6,8 @@ using Microsoft.UI.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game.PathAbstraction;
using System.Collections.Immutable;
using System.Globalization;
using Windows.Graphics;
using Windows.Win32.Foundation;
@@ -24,7 +26,10 @@ internal sealed class LaunchOptions : DbStoreOptions
private readonly int primaryScreenHeight;
private readonly int primaryScreenFps;
private string? gamePath;
private ImmutableList<GamePathEntry>? gamePathEntries;
private bool? isEnabled;
private bool? isAdvancedLaunchOptionsEnabled;
private bool? isFullScreen;
private bool? isBorderless;
private bool? isExclusive;
@@ -36,6 +41,7 @@ internal sealed class LaunchOptions : DbStoreOptions
private int? targetFps;
private NameValue<int>? monitor;
private bool? isMonitorEnabled;
private bool? isUseCloudThirdPartyMobile;
private AspectRatio? selectedAspectRatio;
private bool? useStarwardPlayTimeStatistics;
private bool? setDiscordActivityWhenPlaying;
@@ -85,12 +91,32 @@ internal sealed class LaunchOptions : DbStoreOptions
}
}
public string GamePath
{
get => GetOption(ref gamePath, SettingEntry.GamePath);
set => SetOption(ref gamePath, SettingEntry.GamePath, value);
}
public ImmutableList<GamePathEntry> GamePathEntries
{
// Because DbStoreOptions can't detect collection change, We use
// ImmutableList to imply that the whole list needs to be replaced
get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize<ImmutableList<GamePathEntry>>(raw), []);
set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value));
}
public bool IsEnabled
{
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
}
public bool IsAdvancedLaunchOptionsEnabled
{
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
}
public bool IsFullScreen
{
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
@@ -166,6 +192,12 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref isMonitorEnabled, SettingEntry.LaunchIsMonitorEnabled, value);
}
public bool IsUseCloudThirdPartyMobile
{
get => GetOption(ref isUseCloudThirdPartyMobile, SettingEntry.LaunchIsUseCloudThirdPartyMobile, false);
set => SetOption(ref isUseCloudThirdPartyMobile, SettingEntry.LaunchIsUseCloudThirdPartyMobile, value);
}
public List<AspectRatio> AspectRatios { get; } =
[
new(2560, 1440),

View File

@@ -0,0 +1,87 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.PathAbstraction;
using System.Collections.Immutable;
using System.IO;
namespace Snap.Hutao.Service.Game;
internal static class LaunchOptionsExtension
{
public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = options.GamePath;
gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameFolder))
{
gameFileName = default;
return false;
}
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
return true;
}
public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
{
gamePath = options.GamePath;
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
return true;
}
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
{
string gamePath = options.GamePath;
if (string.IsNullOrEmpty(gamePath))
{
entry = default;
return options.GamePathEntries;
}
if (options.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, options.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed)
{
entry = existed;
return options.GamePathEntries;
}
entry = GamePathEntry.Create(options.GamePath);
return [.. options.GamePathEntries, entry];
}
public static ImmutableList<GamePathEntry> RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected)
{
if (entry is not null)
{
if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase))
{
options.GamePath = string.Empty;
}
options.GamePathEntries = options.GamePathEntries.Remove(entry);
}
return options.GetGamePathEntries(out selected);
}
public static ImmutableList<GamePathEntry> UpdateGamePathAndRefreshEntries(this LaunchOptions options, string gamePath)
{
options.GamePath = gamePath;
ImmutableList<GamePathEntry> entries = options.GetGamePathEntries(out _);
options.GamePathEntries = entries;
return entries;
}
}

View File

@@ -16,12 +16,12 @@ internal sealed partial class GamePackageService : IGamePackageService
{
private readonly PackageConverter packageConverter;
private readonly IServiceProvider serviceProvider;
private readonly LaunchOptions launchOptions;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
{
return false;
}
@@ -58,7 +58,7 @@ internal sealed partial class GamePackageService : IGamePackageService
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
await taskContext.SwitchToMainThreadAsync();
appOptions.GamePath = Path.Combine(gameFolder, exeName);
launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName));
}
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.PathAbstraction;
internal sealed class GamePathEntry
{
[JsonPropertyName("Path")]
public string Path { get; set; } = default!;
[JsonIgnore]
public GamePathKind Kind { get => GetKind(Path); }
public static GamePathEntry Create(string path)
{
return new()
{
Path = path,
};
}
private static GamePathKind GetKind(string path)
{
return GamePathKind.None;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.PathAbstraction;
internal enum GamePathKind
{
None,
ChineseClient,
OverseaClient,
ChineseCloud,
OverseaCloud,
}

View File

@@ -3,19 +3,19 @@
using Snap.Hutao.Service.Game.Locator;
namespace Snap.Hutao.Service.Game;
namespace Snap.Hutao.Service.Game.PathAbstraction;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGamePathService))]
internal sealed partial class GamePathService : IGamePathService
{
private readonly IServiceProvider serviceProvider;
private readonly AppOptions appOptions;
private readonly LaunchOptions launchOptions;
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
{
// Cannot find in setting
if (string.IsNullOrEmpty(appOptions.GamePath))
if (string.IsNullOrEmpty(launchOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
@@ -40,7 +40,7 @@ internal sealed partial class GamePathService : IGamePathService
if (isOk)
{
// Save result.
appOptions.GamePath = path;
launchOptions.UpdateGamePathAndRefreshEntries(path);
}
else
{
@@ -48,9 +48,9 @@ internal sealed partial class GamePathService : IGamePathService
}
}
if (!string.IsNullOrEmpty(appOptions.GamePath))
if (!string.IsNullOrEmpty(launchOptions.GamePath))
{
return new(true, appOptions.GamePath);
return new(true, launchOptions.GamePath);
}
else
{

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
namespace Snap.Hutao.Service.Game.PathAbstraction;
internal interface IGamePathService
{

View File

@@ -21,7 +21,6 @@ internal sealed partial class GameProcessService : IGameProcessService
private readonly IDiscordService discordService;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
private readonly AppOptions appOptions;
private volatile bool isGameRunning;
@@ -52,7 +51,7 @@ internal sealed partial class GameProcessService : IGameProcessService
return;
}
if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
{
ArgumentException.ThrowIfNullOrEmpty(gamePath);
return; // null check passing, actually never reach.
@@ -73,7 +72,7 @@ internal sealed partial class GameProcessService : IGameProcessService
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
}
if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
{
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
try
@@ -116,6 +115,7 @@ internal sealed partial class GameProcessService : IGameProcessService
.AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
.AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
.AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
.AppendIf("-platform_type CLOUD_THIRD_PARTY_MOBILE", launchOptions.IsUseCloudThirdPartyMobile)
.ToString();
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
@@ -20,6 +19,8 @@ internal interface IUserCollectionService
UserGameRole? GetUserGameRoleByUid(string uid);
ValueTask RemoveUserAsync(BindingUser user);
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea);
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromInputCookieAsync(InputCookie inputCookie);
bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user);
}

View File

@@ -1,13 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Service.User;
internal interface IUserInitializationService
{
ValueTask<ViewModel.User.User?> CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken));
ValueTask<ViewModel.User.User?> CreateUserFromInputCookieOrDefaultAsync(InputCookie inputCookie, CancellationToken token = default(CancellationToken));
ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default(CancellationToken));
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
@@ -42,13 +41,7 @@ internal interface IUserService
/// <returns>对应的角色信息</returns>
UserGameRole? GetUserGameRoleByUid(string uid);
/// <summary>
/// 尝试异步处理输入的Cookie
/// </summary>
/// <param name="cookie">Cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <returns>处理的结果</returns>
ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea);
ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(InputCookie inputCookie);
ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user);

View File

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Service.User;
internal sealed class InputCookie : IDeconstruct<Cookie, bool, string?>
{
private InputCookie(Cookie cookie, bool isOversea)
{
Cookie = cookie;
IsOversea = isOversea;
cookie.TryGetDeviceFp(out string? deviceFp);
DeviceFp = deviceFp;
}
private InputCookie(Cookie cookie, bool isOversea, string? deviceFp)
{
Cookie = cookie;
IsOversea = isOversea;
DeviceFp = deviceFp;
}
public Cookie Cookie { get; }
public bool IsOversea { get; }
public string? DeviceFp { get; }
public static InputCookie Create(Cookie cookie, bool isOversea, string? deviceFp)
{
return new InputCookie(cookie, isOversea, deviceFp);
}
public static InputCookie CreateWithDeviceFpInference(Cookie cookie, bool isOversea)
{
return new InputCookie(cookie, isOversea);
}
public void Deconstruct(out Cookie cookie, out bool isOversea, out string? deviceFp)
{
cookie = Cookie;
isOversea = IsOversea;
deviceFp = DeviceFp;
}
}

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Message;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
@@ -113,6 +112,11 @@ internal sealed partial class UserCollectionService : IUserCollectionService
midUserMap?.Remove(user.Entity.Mid);
}
foreach (UserGameRole role in user.UserGameRoles)
{
uidUserGameRoleMap?.Remove(role.GameUid);
}
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false);
@@ -146,14 +150,14 @@ internal sealed partial class UserCollectionService : IUserCollectionService
return midUserMap.TryGetValue(mid, out user);
}
public async ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea)
public async ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromInputCookieAsync(InputCookie inputCookie)
{
await taskContext.SwitchToBackgroundAsync();
BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false);
BindingUser? newUser = await userInitializationService.CreateUserFromInputCookieOrDefaultAsync(inputCookie).ConfigureAwait(false);
if (newUser is null)
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
}
await GetUserCollectionAsync().ConfigureAwait(false);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.PublicData.DeviceFp;
using Snap.Hutao.Web.Response;
@@ -21,7 +22,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
return;
}
if (user.Entity.FingerprintLastUpdateTime >= DateTimeOffset.UtcNow - TimeSpan.FromDays(3))
if (user.Entity.FingerprintLastUpdateTime >= DateTimeOffset.UtcNow - TimeSpan.FromDays(7))
{
if (!string.IsNullOrEmpty(user.Fingerprint))
{
@@ -29,40 +30,62 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
}
}
string model = Core.Random.GetUpperAndNumberString(6);
Dictionary<string, string> extendProperties = new()
string device = Core.Random.GetUpperAndNumberString(12);
string product = Core.Random.GetUpperAndNumberString(6);
Dictionary<string, object> extendProperties = new()
{
{ "cpuType", "arm64-v8a" },
{ "proxyStatus", 0 },
{ "isRoot", 0 },
{ "romCapacity", "512" },
{ "productName", model },
{ "romRemain", "256" },
{ "manufacturer", "XiaoMi" },
{ "appMemory", "512" },
{ "deviceName", device },
{ "productName", product },
{ "romRemain", "512" },
{ "hostname", "dg02-pool03-kvm87" },
{ "screenSize", "1080x1920" },
{ "osVersion", "13" },
{ "screenSize", "1440x2905" },
{ "isTablet", 0 },
{ "aaid", string.Empty },
{ "vendor", "中国移动" },
{ "accelerometer", "1.4883357x7.1712894x6.2847486" },
{ "buildTags", "release-keys" },
{ "model", model },
{ "model", device },
{ "brand", "XiaoMi" },
{ "oaid", string.Empty },
{ "hardware", "qcom" },
{ "deviceType", "OP5913L1" },
{ "devId", "REL" },
{ "serialNumber", "unknown" },
{ "buildTime", "1687848011000" },
{ "buildUser", "root" },
{ "ramCapacity", "469679" },
{ "magnetometer", "20.081251x-27.487501x2.1937501" },
{ "display", $"{model}_13.1.0.181(CN01)" },
{ "ramRemain", "215344" },
{ "deviceInfo", $@"XiaoMi/{model}/OP5913L1:13/SKQ1.221119.001/T.118e6c7-5aa23-73911:user/release-keys" },
{ "gyroscope", "0.030226856x0.014647375x0.010652636" },
{ "sdCapacity", 512215 },
{ "buildTime", "1693626947000" },
{ "buildUser", "android-build" },
{ "simState", 5 },
{ "ramRemain", "239814" },
{ "appUpdateTimeDiff", 1702604034482 },
{ "deviceInfo", $@"XiaoMi\/{product}\/OP5913L1:13\/SKQ1.221119.001\/T.118e6c7-5aa23-73911:user\/release-keys" },
{ "vaid", string.Empty },
{ "buildType", "user" },
{ "sdkVersion", "33" },
{ "sdkVersion", "34" },
{ "ui_mode", "UI_MODE_TYPE_NORMAL" },
{ "isMockLocation", 0 },
{ "cpuType", "arm64-v8a" },
{ "isAirMode", 0 },
{ "ringMode", 2 },
{ "chargeStatus", 1 },
{ "manufacturer", "XiaoMi" },
{ "emulatorStatus", 0 },
{ "appMemory", "512" },
{ "osVersion", "14" },
{ "vendor", "unknown" },
{ "accelerometer", "1.4883357x7.1712894x6.2847486" },
{ "sdRemain", 239600 },
{ "buildTags", "release-keys" },
{ "packageName", "com.mihoyo.hyperion" },
{ "networkType", "WiFi" },
{ "oaid", string.Empty },
{ "debugStatus", 1 },
{ "ramCapacity", "469679" },
{ "magnetometer", "20.081251x-27.487501x2.1937501" },
{ "display", $"{product}_13.1.0.181(CN01)" },
{ "appInstallTimeDiff", 1688455751496 },
{ "packageVersion", "2.20.1" },
{ "gyroscope", "0.030226856x0.014647375x0.010652636" },
{ "batteryStatus", 100 },
{ "hasKeyboard", 0 },
{ "board", "taro" },
};
@@ -79,9 +102,8 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
};
Response<DeviceFpWrapper> response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false);
user.Fingerprint = response.IsOk() ? response.Data.DeviceFp : string.Empty;
user.TryUpdateFingerprint(response.IsOk() ? response.Data.DeviceFp : string.Empty);
user.Entity.FingerprintLastUpdateTime = DateTimeOffset.UtcNow;
user.NeedDbUpdateAfterResume = true;
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Model.Entity.Extension;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Passport;
@@ -30,14 +31,16 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
return user;
}
public async ValueTask<ViewModel.User.User?> CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
public async ValueTask<ViewModel.User.User?> CreateUserFromInputCookieOrDefaultAsync(InputCookie inputCookie, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
(Cookie cookie, bool isOversea, string? deviceFp) = inputCookie;
Model.Entity.User entity = Model.Entity.User.From(cookie, isOversea);
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
entity.Mid = isOversea ? entity.Aid : cookie.GetValueOrDefault(Cookie.MID);
entity.IsOversea = isOversea;
entity.TryUpdateFingerprint(deviceFp);
if (entity.Aid is not null && entity.Mid is not null)
{

View File

@@ -17,15 +17,15 @@ internal enum UserOptionResult
/// <summary>
/// Cookie不完整
/// </summary>
Incomplete,
CookieIncomplete,
/// <summary>
/// Cookie信息已经失效
/// </summary>
Invalid,
CookieInvalid,
/// <summary>
/// 用户的Cookie成功更新
/// </summary>
Updated,
CookieUpdated,
}

View File

@@ -56,33 +56,36 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
return userCollectionService.GetUserGameRoleByUid(uid);
}
public async ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea)
public async ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(InputCookie inputCookie)
{
await taskContext.SwitchToBackgroundAsync();
string? mid = cookie.GetValueOrDefault(isOversea ? Cookie.STUID : Cookie.MID);
(Cookie cookie, bool isOversea, string? deviceFp) = inputCookie;
if (string.IsNullOrEmpty(mid))
string? midOrAid = cookie.GetValueOrDefault(isOversea ? Cookie.STUID : Cookie.MID);
if (string.IsNullOrEmpty(midOrAid))
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoMid);
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoMid);
}
// 检查 mid 对应用户是否存在
if (!userCollectionService.TryGetUserByMid(mid, out BindingUser? user))
if (!userCollectionService.TryGetUserByMid(midOrAid, out BindingUser? user))
{
return await userCollectionService.TryCreateAndAddUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false);
return await userCollectionService.TryCreateAndAddUserFromInputCookieAsync(inputCookie).ConfigureAwait(false);
}
if (!cookie.TryGetSToken(isOversea, out Cookie? stoken))
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoSToken);
}
user.SToken = stoken;
user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken;
user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
user.TryUpdateFingerprint(deviceFp);
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
return new(UserOptionResult.Updated, mid);
return new(UserOptionResult.CookieUpdated, midOrAid);
}
public async ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user)

View File

@@ -299,13 +299,14 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.1">
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -16,6 +16,6 @@
<Image
Width="320"
Height="320"
Source="{x:Bind QRCodeSource}"/>
Source="{x:Bind QRCodeSource, Mode=OneWay}"/>
</StackPanel>
</ContentDialog>
</ContentDialog>

View File

@@ -71,7 +71,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
private async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenCoreAsync()
{
await taskContext.SwitchToMainThreadAsync();
await ShowAsync();
_ = ShowAsync();
while (!userManualCancellationTokenSource.IsCancellationRequested)
{

View File

@@ -183,7 +183,7 @@
</DataTemplate>
<DataTemplate x:Key="CardTemplate" x:DataType="shvcp:CardReference">
<ItemContainer cw:Effects.Shadow="{StaticResource CompatCardShadow}" Child="{Binding Card}">
<ItemContainer cw:Effects.Shadow="{ThemeResource CompatCardShadow}" Child="{Binding Card}">
<ItemContainer.Resources>
<SolidColorBrush x:Key="ItemContainerPointerOverBackground" Color="Transparent"/>
</ItemContainer.Resources>

View File

@@ -7,7 +7,6 @@ using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Bridge;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Page;
@@ -30,11 +29,11 @@ internal interface ISupportLoginByWebView
}
}
static async ValueTask PostHandleCurrentCookieAsync(IServiceProvider serviceProvider, Cookie cookie, bool isOversea)
static async ValueTask PostHandleCurrentCookieAsync(IServiceProvider serviceProvider, InputCookie inputCookie)
{
(UserOptionResult result, string nickname) = await serviceProvider
.GetRequiredService<IUserService>()
.ProcessInputCookieAsync(cookie, isOversea)
.ProcessInputCookieAsync(inputCookie)
.ConfigureAwait(false);
serviceProvider.GetRequiredService<INavigationService>().GoBack();

View File

@@ -38,7 +38,12 @@
<DataTemplate x:Key="GameAccountListTemplate">
<Grid>
<StackPanel Margin="0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,12">
<TextBlock Text="{Binding Name}"/>
<TextBlock
Opacity="0.8"
@@ -47,9 +52,9 @@
</StackPanel>
<StackPanel
x:Name="ButtonPanel"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal"
Visibility="Collapsed">
Orientation="Horizontal">
<Button
MinWidth="48"
Margin="4,8"
@@ -78,264 +83,293 @@
FontFamily="{StaticResource SymbolThemeFontFamily}"
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRemoveToolTip}"/>
</StackPanel>
</Grid>
</DataTemplate>
<Grid.Resources>
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
<DataTemplate x:Key="GamePathEntryListTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Padding="0,16"
VerticalAlignment="Center"
Text="{Binding Path}"/>
<Button
Grid.Column="1"
MinWidth="48"
Margin="4,8,0,8"
VerticalAlignment="Stretch"
Command="{Binding DataContext.RemoveGamePathEntryCommand, Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
Content="&#xE74D;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRemoveToolTip}"/>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid>
<Grid
Height="{StaticResource AppBarThemeCompactHeight}"
VerticalAlignment="Top"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
IsHitTestVisible="False"/>
<Pivot>
<Pivot.RightHeader>
<CommandBar DefaultLabelPosition="Right">
<CommandBar.Content>
<TextBlock
Margin="12,14,12,0"
VerticalAlignment="Center"
Text="{Binding LaunchStatusOptions.LaunchStatus.Description}"/>
</CommandBar.Content>
<AppBarButton
Command="{Binding OpenScreenshotFolderCommand}"
Icon="{shcm:FontIcon Glyph=&#xED25;}"
Label="{shcm:ResourceString Name=ViewPageOpenScreenshotFolderAction}"/>
<AppBarButton
Command="{Binding LaunchCommand}"
Icon="{shcm:FontIcon Glyph=&#xE7FC;}"
Label="{shcm:ResourceString Name=ViewPageLaunchGameAction}"/>
</CommandBar>
</Pivot.RightHeader>
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid
Height="{StaticResource AppBarThemeCompactHeight}"
VerticalAlignment="Top"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
IsHitTestVisible="False"/>
<Pivot>
<Pivot.RightHeader>
<CommandBar DefaultLabelPosition="Right">
<CommandBar.Content>
<TextBlock
Margin="12,14,12,0"
VerticalAlignment="Center"
Text="{Binding LaunchStatusOptions.LaunchStatus.Description}"/>
</CommandBar.Content>
<AppBarButton
Command="{Binding OpenScreenshotFolderCommand}"
Icon="{shcm:FontIcon Glyph=&#xED25;}"
Label="{shcm:ResourceString Name=ViewPageOpenScreenshotFolderAction}"/>
<AppBarButton
Command="{Binding ResetGamePathCommand}"
Icon="{shcm:FontIcon Glyph=&#xEBC4;}"
Label="{shcm:ResourceString Name=ViewPageResetGamePathAction}"/>
<AppBarButton
Command="{Binding LaunchCommand}"
Icon="{shcm:FontIcon Glyph=&#xE7FC;}"
Label="{shcm:ResourceString Name=ViewPageLaunchGameAction}"/>
</CommandBar>
</Pivot.RightHeader>
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameOptionsHeader}">
<ScrollViewer Grid.RowSpan="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="1000"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="16" Spacing="{StaticResource SettingsCardSpacing}">
<InfoBar
IsClosable="False"
IsOpen="True"
Message="{shcm:ResourceString Name=ViewPageLaunchGameConfigurationSaveHint}"
Severity="Informational"/>
<!-- 文件 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameFileHeader}"/>
<cwc:SettingsCard
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8AB;}"
IsEnabled="{Binding RuntimeOptions.IsElevated}">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeDescription}"/>
<TextBlock
Foreground="{ThemeResource SystemControlErrorTextForegroundBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
</StackPanel>
</cwc:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shc:SizeRestrictedContentControl>
<shccs:ComboBox2
DisplayMemberPath="DisplayName"
EnableMemberPath="IsNotCompatOnly"
ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</StackPanel>
</cwc:SettingsCard>
<!-- 注册表 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameRegistryHeader}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDetectAction}"
Command="{Binding DetectGameAccountCommand}"
Description="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE748;}"
IsClickEnabled="True"/>
<Border Style="{StaticResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border>
<!-- 进程 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameProcessHeader}"/>
<cwc:SettingsExpander
shch:SettingsExpanderHelper.IsItemsEnabled="{Binding LaunchOptions.IsEnabled}"
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE943;}"
IsExpanded="True">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
<cwc:SettingsExpander.Items>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
<ComboBox
ItemsSource="{Binding LaunchOptions.AspectRatios}"
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
<StackPanel Orientation="Horizontal" Spacing="16">
<NumberBox
Width="156"
Padding="12,6,0,0"
VerticalAlignment="Center"
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
<StackPanel Orientation="Horizontal" Spacing="16">
<NumberBox
Width="156"
Padding="12,6,0,0"
VerticalAlignment="Center"
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
<StackPanel Orientation="Horizontal" Spacing="16">
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
ItemsSource="{Binding LaunchOptions.Monitors}"
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE785;}"
IsEnabled="{Binding RuntimeOptions.IsElevated}"
Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<NumberBox
MinWidth="156"
Padding="10,8,0,0"
Maximum="720"
Minimum="60"
SpinButtonPlacementMode="Inline"
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
<ToggleSwitch
Width="120"
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
</StackPanel>
</cwc:SettingsCard>
<!-- 进程间 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameInterProcessHeader}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xEC92;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8CF;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
</cwc:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</PivotItem>
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceHeader}">
<Grid>
<ScrollViewer Visibility="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolConverter}}">
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameOptionsHeader}">
<ScrollViewer Grid.RowSpan="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="1000"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<shvc:LaunchGameResourceExpander
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
DataContext="{Binding GameResource.PreDownloadGame.Latest, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourcePreDownloadHeader}"
Visibility="{Binding FallbackValue={StaticResource VisibilityCollapsed}, Converter={StaticResource EmptyObjectToVisibilityConverter}}"/>
<ItemsControl
Margin="0,0,0,0"
ItemTemplate="{StaticResource GameResourceTemplate}"
ItemsSource="{Binding GameResource.PreDownloadGame.Diffs, Mode=OneWay}"/>
<shvc:LaunchGameResourceExpander
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
DataContext="{Binding GameResource.Game.Latest, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceLatestHeader}"/>
<ItemsControl
Margin="0,0,0,16"
ItemTemplate="{StaticResource GameResourceTemplate}"
ItemsSource="{Binding GameResource.Game.Diffs, Mode=OneWay}"/>
<StackPanel Margin="16" Spacing="{StaticResource SettingsCardSpacing}">
<InfoBar
IsClosable="False"
IsOpen="True"
Message="{shcm:ResourceString Name=ViewPageLaunchGameConfigurationSaveHint}"
Severity="Informational"/>
<!-- 文件 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameFileHeader}"/>
<cwc:SettingsCard
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8AB;}"
IsEnabled="{Binding RuntimeOptions.IsElevated}">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeDescription}"/>
<TextBlock
Foreground="{ThemeResource SystemControlErrorTextForegroundBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
</StackPanel>
</cwc:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shc:SizeRestrictedContentControl>
<shccs:ComboBox2
DisplayMemberPath="DisplayName"
EnableMemberPath="IsNotCompatOnly"
ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</StackPanel>
</cwc:SettingsCard>
<!-- 注册表 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameRegistryHeader}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDetectAction}"
Command="{Binding DetectGameAccountCommand}"
Description="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE748;}"
IsClickEnabled="True"/>
<Border Style="{StaticResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border>
<!-- 进程 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameProcessHeader}"/>
<cwc:SettingsExpander
shch:SettingsExpanderHelper.IsItemsEnabled="{Binding LaunchOptions.IsEnabled}"
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE943;}"
IsExpanded="True">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
<cwc:SettingsExpander.Items>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription}" Header="-platform_type CLOUD_THIRD_PARTY_MOBILE">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsUseCloudThirdPartyMobile, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
<ComboBox
ItemsSource="{Binding LaunchOptions.AspectRatios}"
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
<StackPanel Orientation="Horizontal" Spacing="16">
<NumberBox
Width="156"
Padding="12,6,0,0"
VerticalAlignment="Center"
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
<StackPanel Orientation="Horizontal" Spacing="16">
<NumberBox
Width="156"
Padding="12,6,0,0"
VerticalAlignment="Center"
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
<StackPanel Orientation="Horizontal" Spacing="16">
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
ItemsSource="{Binding LaunchOptions.Monitors}"
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE785;}"
IsEnabled="{Binding RuntimeOptions.IsElevated}"
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<NumberBox
MinWidth="156"
Padding="10,8,0,0"
Maximum="720"
Minimum="60"
SpinButtonPlacementMode="Inline"
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
<ToggleSwitch
Width="120"
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
</StackPanel>
</cwc:SettingsCard>
<!-- 进程间 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameInterProcessHeader}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xEC92;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8CF;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
</cwc:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
<shvc:LoadingView IsLoading="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
</Grid>
</PivotItem>
</Pivot>
</PivotItem>
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceHeader}">
<Grid>
<ScrollViewer Visibility="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolConverter}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="1000"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<shvc:LaunchGameResourceExpander
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
DataContext="{Binding GameResource.PreDownloadGame.Latest, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourcePreDownloadHeader}"
Visibility="{Binding FallbackValue={StaticResource VisibilityCollapsed}, Converter={StaticResource EmptyObjectToVisibilityConverter}}"/>
<ItemsControl
Margin="0,0,0,0"
ItemTemplate="{StaticResource GameResourceTemplate}"
ItemsSource="{Binding GameResource.PreDownloadGame.Diffs, Mode=OneWay}"/>
<shvc:LaunchGameResourceExpander
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
DataContext="{Binding GameResource.Game.Latest, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceLatestHeader}"/>
<ItemsControl
Margin="0,0,0,16"
ItemTemplate="{StaticResource GameResourceTemplate}"
ItemsSource="{Binding GameResource.Game.Diffs, Mode=OneWay}"/>
</StackPanel>
</Grid>
</ScrollViewer>
<shvc:LoadingView IsLoading="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
</Grid>
</PivotItem>
</Pivot>
</Grid>
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}">
<StackPanel
MaxWidth="600"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="3">
<Border Style="{ThemeResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GamePathEntryListTemplate}"
ItemsSource="{Binding GamePathEntries}"
SelectedItem="{Binding SelectedGamePathEntry, Mode=TwoWay}"/>
</Border>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
Command="{Binding SetGamePathCommand}"
Header="{shcm:ResourceString Name=ViewPageSettingSetGamePathHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
</StackPanel>
</Grid>
</Grid>
</shc:ScopedPage>

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
@@ -88,7 +89,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
Cookie stokenV1 = Cookie.FromSToken(uid, multiTokenMap[Cookie.STOKEN]);
await ISupportLoginByWebView
.PostHandleCurrentCookieAsync(serviceProvider, stokenV1, true)
.PostHandleCurrentCookieAsync(serviceProvider, InputCookie.Create(stokenV1, true, default))
.ConfigureAwait(false);
}

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
@@ -75,7 +76,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie stokenV2 = Cookie.FromLoginResult(loginResultResponse.Data);
await ISupportLoginByWebView
.PostHandleCurrentCookieAsync(serviceProvider, stokenV2, false)
.PostHandleCurrentCookieAsync(serviceProvider, InputCookie.Create(stokenV2, false, default))
.ConfigureAwait(false);
}
}

View File

@@ -307,20 +307,6 @@
</cwc:SettingsExpander>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGameHeader}"/>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
Command="{Binding SetGamePathCommand}"
Header="{shcm:ResourceString Name=ViewPageSettingSetGamePathHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
<TextBlock Text="{Binding AppOptions.GamePath}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
@@ -418,13 +404,13 @@
<TextBlock
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Text="{shcm:ResourceString Name=ViewPageSettingFeaturesDangerousHint}"
Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription}" Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription}" Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
</StackPanel>
</cwc:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<shvc:Elevation Visibility="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<ToggleSwitch Width="120" IsOn="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard
@@ -435,10 +421,7 @@
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Text="{shcm:ResourceString Name=ViewPageSettingDangerousHint}"
Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock Foreground="{ThemeResource SystemFillColorCriticalBrush}" Text="{shcm:ResourceString Name=ViewPageSettingDangerousHint}"/>
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingDeleteUserDescription}"/>
</StackPanel>
</cwc:SettingsCard.Description>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core;
@@ -11,13 +12,15 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
@@ -38,7 +41,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly IContentDialogFactory contentDialogFactory;
private readonly LaunchStatusOptions launchStatusOptions;
private readonly INavigationService navigationService;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService;
private readonly ResourceClient resourceClient;
@@ -54,6 +57,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private ObservableCollection<GameAccount>? gameAccounts;
private GameAccount? selectedGameAccount;
private GameResource? gameResource;
private bool gamePathSelectedAndValid;
private ImmutableList<GamePathEntry> gamePathEntries;
private GamePathEntry? selectedGamePathEntry;
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
@@ -99,68 +105,132 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
protected override async ValueTask<bool> InitializeUIAsync()
public bool GamePathSelectedAndValid
{
if (File.Exists(AppOptions.GamePath))
get => gamePathSelectedAndValid;
set
{
try
if (SetProperty(ref gamePathSelectedAndValid, value) && value)
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
InitializeUICoreAsync().SafeForget();
}
}
}
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
public GamePathEntry? SelectedGamePathEntry
{
get => selectedGamePathEntry;
set => UpdateSelectedGamePathEntry(value, true);
}
protected override ValueTask<bool> InitializeUIAsync()
{
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SelectedGamePathEntry = entry;
return ValueTask.FromResult(true);
}
private async ValueTask InitializeUICoreAsync()
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
{
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
try
{
try
SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
if (!IgnoredInvalidChannelOptions.Contains(options))
{
SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
throw new NotSupportedException($"不支持的 MultiChannel: {options}");
}
// 后台收集
throw new NotSupportedException($"不支持的 MultiChannel: {options}");
}
}
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
}
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
// Sync uid
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
{
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
}
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
catch (OperationCanceledException)
{
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
}
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
// Sync uid
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
{
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
}
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
}
else
catch (UserdataCorruptedException ex)
{
infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid);
await taskContext.SwitchToMainThreadAsync();
await navigationService
.NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
infoBarService.Error(ex);
}
catch (OperationCanceledException)
{
}
}
private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack)
{
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)) && setBack)
{
if (IsViewDisposed)
{
return;
}
launchOptions.GamePath = value?.Path ?? string.Empty;
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
}
}
[Command("SetGamePathCommand")]
private async Task SetGamePathAsync()
{
IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
return true;
await taskContext.SwitchToMainThreadAsync();
try
{
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
}
catch (SqliteException ex)
{
// 文件夹权限不足,无法写入数据库
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
}
}
[Command("ResetGamePathCommand")]
private void ResetGamePath()
{
SelectedGamePathEntry = default;
}
[Command("RemoveGamePathEntryCommand")]
private void RemoveGamePathEntry(GamePathEntry? entry)
{
GamePathEntries = launchOptions.RemoveGamePathEntry(entry, out GamePathEntry? selected);
SelectedGamePathEntry = selected;
}
[Command("LaunchCommand")]
@@ -187,6 +257,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
return;
}
else
{
await taskContext.SwitchToMainThreadAsync();
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
UpdateSelectedGamePathEntry(entry, false);
}
}
if (SelectedGameAccount is not null)
@@ -265,7 +341,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
[Command("OpenScreenshotFolderCommand")]
private async Task OpenScreenshotFolderAsync()
{
string game = appOptions.GamePath;
string game = LaunchOptions.GamePath;
string? directory = Path.GetDirectoryName(game);
ArgumentException.ThrowIfNullOrEmpty(directory);
string screenshot = Path.Combine(directory, "ScreenShot");

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
@@ -16,6 +15,7 @@ using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
@@ -53,6 +53,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private readonly HutaoUserOptions hutaoUserOptions;
private readonly IInfoBarService infoBarService;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
private readonly HotKeyOptions hotKeyOptions;
private readonly IUserService userService;
private readonly ITaskContext taskContext;
@@ -74,6 +75,8 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
public LaunchOptions LaunchOptions { get => launchOptions; }
public HutaoPassportViewModel Passport { get => hutaoPassportViewModel; }
public NameValue<BackdropType>? SelectedBackdropType
@@ -157,27 +160,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
}
[Command("SetGamePathCommand")]
private async Task SetGamePathAsync()
{
IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
if (isOk)
{
await taskContext.SwitchToMainThreadAsync();
try
{
AppOptions.GamePath = path;
}
catch (SqliteException ex)
{
// 文件夹权限不足,无法写入数据库
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
}
}
}
[Command("SetPowerShellPathCommand")]
private async Task SetPowerShellPathAsync()
{
@@ -193,7 +175,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
[Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache()
{
string gamePath = AppOptions.GamePath;
string gamePath = launchOptions.GamePath;
if (!string.IsNullOrEmpty(gamePath))
{

View File

@@ -55,7 +55,7 @@ internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, IMapping
set => SetSelectedUserGameRole(value);
}
public string? Fingerprint { get => inner.Fingerprint; set => inner.Fingerprint = value; }
public string? Fingerprint { get => inner.Fingerprint; }
public Guid InnerId { get => inner.InnerId; }
@@ -111,4 +111,4 @@ internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, IMapping
messenger.Send(Message.UserChangedMessage.CreateOnlyRoleChanged(this));
}
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Extension;
namespace Snap.Hutao.ViewModel.User;
internal static class UserExtension
{
public static bool TryUpdateFingerprint(this User user, string? deviceFp)
{
return user.Entity.TryUpdateFingerprint(deviceFp);
}
}

View File

@@ -52,8 +52,11 @@ internal sealed partial class UserViewModel : ObservableObject
get => selectedUser ??= userService.Current;
set
{
// Pre select the chosen role to avoid multiple UserChangedMessage
value?.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
if (value is { SelectedUserGameRole: null })
{
// Pre select the chosen role to avoid multiple UserChangedMessage
value.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
}
if (SetProperty(ref selectedUser, value))
{
@@ -87,13 +90,13 @@ internal sealed partial class UserViewModel : ObservableObject
infoBarService.Success(SH.FormatViewModelUserAdded(uid));
break;
case UserOptionResult.Incomplete:
case UserOptionResult.CookieIncomplete:
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
case UserOptionResult.CookieInvalid:
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
case UserOptionResult.CookieUpdated:
infoBarService.Success(SH.FormatViewModelUserUpdated(uid));
break;
default:
@@ -118,16 +121,16 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("AddUserCommand")]
private Task AddUserAsync()
{
return AddUserCoreAsync(false).AsTask();
return AddUserByManualInputCookieAsync(false).AsTask();
}
[Command("AddOverseaUserCommand")]
private Task AddOverseaUserAsync()
{
return AddUserCoreAsync(true).AsTask();
return AddUserByManualInputCookieAsync(true).AsTask();
}
private async ValueTask AddUserCoreAsync(bool isOversea)
private async ValueTask AddUserByManualInputCookieAsync(bool isOversea)
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
@@ -140,9 +143,7 @@ internal sealed partial class UserViewModel : ObservableObject
if (result.TryGetValue(out string rawCookie))
{
Cookie cookie = Cookie.Parse(rawCookie);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateWithDeviceFpInference(cookie, isOversea)).ConfigureAwait(false);
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
}
}
@@ -150,22 +151,21 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("LoginMihoyoUserCommand")]
private void LoginMihoyoUser()
{
if (runtimeOptions.IsWebView2Supported)
{
navigationService.Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
}
else
{
infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
}
NavigateToLoginPage<LoginMihoyoUserPage>();
}
[Command("LoginHoyoverseUserCommand")]
private void LoginHoyoverseUser()
{
NavigateToLoginPage<LoginHoyoverseUserPage>();
}
private void NavigateToLoginPage<TPage>()
where TPage : Page
{
if (runtimeOptions.IsWebView2Supported)
{
navigationService.Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
navigationService.Navigate<TPage>(INavigationAwaiter.Default);
}
else
{
@@ -192,9 +192,7 @@ internal sealed partial class UserViewModel : ObservableObject
if (sTokenResponse.IsOk())
{
Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateWithDeviceFpInference(stokenV2, false)).ConfigureAwait(false);
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
}
}
@@ -202,17 +200,19 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("RemoveUserCommand")]
private async Task RemoveUserAsync(User? user)
{
if (user is not null)
if (user is null)
{
try
{
await userService.RemoveUserAsync(user).ConfigureAwait(false);
infoBarService.Success(SH.FormatViewModelUserRemoved(user.UserInfo?.Nickname));
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
return;
}
try
{
await userService.RemoveUserAsync(user).ConfigureAwait(false);
infoBarService.Success(SH.FormatViewModelUserRemoved(user.UserInfo?.Nickname));
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
}
@@ -243,44 +243,42 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("RefreshCookieTokenCommand")]
private async Task RefreshCookieTokenAsync()
{
if (SelectedUser is not null)
if (SelectedUser is null)
{
if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess);
}
else
{
infoBarService.Warning(SH.ViewUserRefreshCookieTokenWarning);
}
return;
}
if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess);
}
else
{
infoBarService.Warning(SH.ViewUserRefreshCookieTokenWarning);
}
}
[Command("ClaimSignInRewardCommand")]
private async Task ClaimSignInRewardAsync(AppBarButton? appBarButton)
{
if (SelectedUser is not null)
if (!UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid))
{
if (UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid))
{
(bool isOk, string message) = await signInService.ClaimRewardAsync(userAndUid).ConfigureAwait(false);
if (isOk)
{
infoBarService.Success(message);
}
else
{
await taskContext.SwitchToMainThreadAsync();
FlyoutBase.ShowAttachedFlyout(appBarButton);
infoBarService.Warning(message);
}
}
else
{
infoBarService.Warning(SH.MustSelectUserAndUid);
}
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
(bool isOk, string message) = await signInService.ClaimRewardAsync(userAndUid).ConfigureAwait(false);
if (isOk)
{
infoBarService.Success(message);
return;
}
// Manual webview
await taskContext.SwitchToMainThreadAsync();
FlyoutBase.ShowAttachedFlyout(appBarButton);
infoBarService.Warning(message);
}
[Command("OpenDocumentationCommand")]

View File

@@ -120,9 +120,11 @@ internal static class ApiEndpoints
/// <returns>游戏记录实时便笺字符串</returns>
public static string GameRecordDailyNote(in PlayerUid uid)
{
return $"{ApiTakumiRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
return $"{GameRecordDailyNotePath}?server={uid.Region}&role_id={uid.Value}";
}
public const string GameRecordDailyNotePath = $"{ApiTakumiRecordApi}/dailyNote";
/// <summary>
/// 游戏记录主页
/// </summary>
@@ -130,9 +132,11 @@ internal static class ApiEndpoints
/// <returns>游戏记录主页字符串</returns>
public static string GameRecordIndex(in PlayerUid uid)
{
return $"{ApiTakumiRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
return $"{GameRecordIndexPath}?server={uid.Region}&role_id={uid.Value}";
}
public const string GameRecordIndexPath = $"{ApiTakumiRecordApi}/index";
/// <summary>
/// 深渊信息
/// </summary>
@@ -141,8 +145,10 @@ internal static class ApiEndpoints
/// <returns>深渊信息字符串</returns>
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.SpiralAbyssSchedule scheduleType, in PlayerUid uid)
{
return $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
return $"{GameRecordSpiralAbyssPath}?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
}
public const string GameRecordSpiralAbyssPath = $"{ApiTakumiRecordApi}/spiralAbyss";
#endregion
#region ApiTakumiEventCalculate

View File

@@ -14,19 +14,12 @@ internal sealed partial class Cookie
{
private readonly SortedDictionary<string, string> inner;
/// <summary>
/// 构造一个空白的Cookie
/// </summary>
public Cookie()
: this([])
{
}
/// <summary>
/// 构造一个新的Cookie
/// </summary>
/// <param name="dict">源</param>
private Cookie(SortedDictionary<string, string> dict)
public Cookie(SortedDictionary<string, string> dict)
{
inner = dict;
}
@@ -37,11 +30,6 @@ internal sealed partial class Cookie
set => inner[key] = value;
}
/// <summary>
/// 解析Cookie字符串
/// </summary>
/// <param name="cookieString">cookie字符串</param>
/// <returns>新的Cookie对象</returns>
public static Cookie Parse(string cookieString)
{
SortedDictionary<string, string> cookieMap = [];
@@ -69,11 +57,6 @@ internal sealed partial class Cookie
return new(cookieMap);
}
/// <summary>
/// 从登录结果创建 Cookie
/// </summary>
/// <param name="loginResult">登录结果</param>
/// <returns>Cookie</returns>
public static Cookie FromLoginResult(LoginResult? loginResult)
{
if (loginResult is null)
@@ -91,12 +74,6 @@ internal sealed partial class Cookie
return new(cookieMap);
}
/// <summary>
/// 从 SToken 创建 Cookie
/// </summary>
/// <param name="stuid">stuid</param>
/// <param name="stoken">stoken</param>
/// <returns>Cookie</returns>
public static Cookie FromSToken(string stuid, string stoken)
{
SortedDictionary<string, string> cookieMap = new()
@@ -113,119 +90,18 @@ internal sealed partial class Cookie
return inner.Count <= 0;
}
public bool TryGetLoginTicket([NotNullWhen(true)] out Cookie? cookie)
{
if (TryGetValue(LOGIN_TICKET, out string? loginTicket) && TryGetValue(LOGIN_UID, out string? loginUid))
{
cookie = new Cookie(new()
{
[LOGIN_TICKET] = loginTicket,
[LOGIN_UID] = loginUid,
});
return true;
}
cookie = null;
return false;
}
public bool TryGetSToken(bool isOversea, [NotNullWhen(true)] out Cookie? cookie)
{
return isOversea ? TryGetLegacySToken(out cookie) : TryGetSToken(out cookie);
}
public bool TryGetLToken([NotNullWhen(true)] out Cookie? cookie)
{
if (TryGetValue(LTOKEN, out string? ltoken) && TryGetValue(LTUID, out string? ltuid))
{
cookie = new Cookie(new()
{
[LTOKEN] = ltoken,
[LTUID] = ltuid,
});
return true;
}
cookie = null;
return false;
}
public bool TryGetCookieToken([NotNullWhen(true)] out Cookie? cookie)
{
if (TryGetValue(ACCOUNT_ID, out string? accountId) && TryGetValue(COOKIE_TOKEN, out string? cookieToken))
{
cookie = new Cookie(new()
{
[ACCOUNT_ID] = accountId,
[COOKIE_TOKEN] = cookieToken,
});
return true;
}
cookie = null;
return false;
}
/// <inheritdoc cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
return inner.TryGetValue(key, out value);
}
/// <summary>
/// 获取值
/// </summary>
/// <param name="key">键</param>
/// <returns>值或默认值</returns>
public string? GetValueOrDefault(string key)
{
return inner.GetValueOrDefault(key);
}
/// <summary>
/// 转换为Cookie的字符串表示
/// </summary>
/// <returns>Cookie的字符串表示</returns>
public override string ToString()
{
return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
private bool TryGetSToken([NotNullWhen(true)] out Cookie? cookie)
{
if (TryGetValue(MID, out string? mid) && TryGetValue(STOKEN, out string? stoken) && TryGetValue(STUID, out string? stuid))
{
cookie = new Cookie(new()
{
[MID] = mid,
[STOKEN] = stoken,
[STUID] = stuid,
});
return true;
}
cookie = null;
return false;
}
private bool TryGetLegacySToken([NotNullWhen(true)] out Cookie? cookie)
{
if (TryGetValue(STOKEN, out string? stoken) && TryGetValue(STUID, out string? stuid))
{
cookie = new Cookie(new()
{
[STOKEN] = stoken,
[STUID] = stuid,
});
return true;
}
cookie = null;
return false;
return inner.JoinToString(';', (builder, key, value) => builder.Append(key).Append('=').Append(value));
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
[SuppressMessage("", "SA1310")]
internal static class CookieExtension
{
private const string LOGIN_TICKET = "login_ticket";
private const string LOGIN_UID = "login_uid";
private const string ACCOUNT_ID = "account_id";
private const string COOKIE_TOKEN = "cookie_token";
private const string LTOKEN = "ltoken";
private const string LTUID = "ltuid";
private const string MID = "mid";
private const string STOKEN = "stoken";
private const string STUID = "stuid";
private const string DEVICEFP = "DEVICEFP";
public static bool TryGetLoginTicket(this Cookie source, [NotNullWhen(true)] out Cookie? cookie)
{
return source.TryGetValuesToCookie([LOGIN_TICKET, LOGIN_UID], out cookie);
}
public static bool TryGetSToken(this Cookie source, bool isOversea, [NotNullWhen(true)] out Cookie? cookie)
{
return isOversea
? source.TryGetValuesToCookie([STOKEN, STUID], out cookie)
: source.TryGetValuesToCookie([MID, STOKEN, STUID], out cookie);
}
public static bool TryGetLToken(this Cookie source, [NotNullWhen(true)] out Cookie? cookie)
{
return source.TryGetValuesToCookie([LTOKEN, LTUID], out cookie);
}
public static bool TryGetCookieToken(this Cookie source, [NotNullWhen(true)] out Cookie? cookie)
{
return source.TryGetValuesToCookie([ACCOUNT_ID, COOKIE_TOKEN], out cookie);
}
public static bool TryGetDeviceFp(this Cookie source, [NotNullWhen(true)] out string? deviceFp)
{
return source.TryGetValue(DEVICEFP, out deviceFp);
}
private static bool TryGetValuesToCookie(this Cookie source, in ReadOnlySpan<string> keys, [NotNullWhen(true)] out Cookie? cookie)
{
Must.Range(keys.Length > 0, "Empty keys is not supported");
SortedDictionary<string, string> cookieMap = [];
foreach (string key in keys)
{
if (source.TryGetValue(key, out string? value))
{
cookieMap.TryAdd(key, value);
}
else
{
cookie = default;
return false;
}
}
cookie = new(cookieMap);
return true;
}
}

View File

@@ -32,6 +32,8 @@ internal static class HoyolabOptions
/// </summary>
public const string MobileUserAgentOversea = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{SaltConstants.OSVersion}";
public const string ToolVersion = "v4.2.2-ys";
/// <summary>
/// 米游社设备Id
/// </summary>

View File

@@ -5,7 +5,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;

View File

@@ -25,17 +25,13 @@ internal sealed partial class CardClient
private readonly ILogger<CardClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 注册验证码
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>注册结果</returns>
public async ValueTask<Response<VerificationRegistration>> CreateVerificationAsync(User user, CancellationToken token)
public async ValueTask<Response<VerificationRegistration>> CreateVerificationAsync(User user, CardVerifiationHeaders headers, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.CardCreateVerification(true))
.SetUserCookieAndFpHeader(user, CookieType.LToken)
.SetHeader("x-rpc-challenge_game", $"{headers.ChallengeGame}")
.SetHeader("x-rpc-challenge_path", headers.ChallengePath)
.Get();
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
@@ -47,17 +43,12 @@ internal sealed partial class CardClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 二次验证
/// </summary>
/// <param name="challenge">流水号</param>
/// <param name="validate">验证</param>
/// <param name="token">取消令牌</param>
/// <returns>验证结果</returns>
public async ValueTask<Response<VerificationResult>> VerifyVerificationAsync(string challenge, string validate, CancellationToken token)
public async ValueTask<Response<VerificationResult>> VerifyVerificationAsync(CardVerifiationHeaders headers, string challenge, string validate, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.CardVerifyVerification)
.SetHeader("x-rpc-challenge_game", $"{headers.ChallengeGame}")
.SetHeader("x-rpc-challenge_path", headers.ChallengePath)
.PostJson(new VerificationData(challenge, validate));
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);

View File

@@ -0,0 +1,44 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
internal sealed class CardVerifiationHeaders
{
public int ChallengeGame { get; private set; }
public string ChallengePath { get; private set; } = string.Empty;
public string Page { get; private set; } = string.Empty;
public static CardVerifiationHeaders CreateForDailyNote()
{
return Create(ApiEndpoints.GameRecordDailyNotePath);
}
public static CardVerifiationHeaders CreateForIndex()
{
return Create(ApiEndpoints.GameRecordIndexPath);
}
public static CardVerifiationHeaders CreateForSpiralAbyss()
{
return Create(ApiEndpoints.GameRecordSpiralAbyssPath);
}
public static CardVerifiationHeaders CreateForCharacter()
{
return Create(ApiEndpoints.GameRecordCharacter, $"{HoyolabOptions.ToolVersion}_#/ys/role/all");
}
private static CardVerifiationHeaders Create(string path, string page = $"{HoyolabOptions.ToolVersion}_#/ys")
{
return new()
{
ChallengeGame = 2,
ChallengePath = path,
Page = page,
};
}
}

View File

@@ -28,12 +28,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
private readonly ILogger<GameRecordClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.X4)]
public async ValueTask<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
{
@@ -54,9 +48,11 @@ internal sealed partial class GameRecordClient : IGameRecordClient
{
// Replace message
resp.Message = SH.WebDailyNoteVerificationFailed;
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForDailyNote();
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
{
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid))
@@ -76,19 +72,12 @@ internal sealed partial class GameRecordClient : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
public async ValueTask<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
@@ -103,14 +92,15 @@ internal sealed partial class GameRecordClient : IGameRecordClient
{
// Replace message
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForIndex();
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
{
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.Get();
@@ -139,7 +129,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
@@ -154,14 +143,15 @@ internal sealed partial class GameRecordClient : IGameRecordClient
{
// Replace message
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForSpiralAbyss();
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
{
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.Get();
@@ -189,7 +179,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordRoleBasicInfo(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
@@ -215,7 +204,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordCharacter)
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id)));
@@ -230,14 +218,15 @@ internal sealed partial class GameRecordClient : IGameRecordClient
{
// Replace message
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForCharacter();
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
{
HttpRequestMessageBuilder verifiedBuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordCharacter)
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id)));

View File

@@ -13,9 +13,9 @@ internal sealed partial class HomaGeetestCardVerifier : IGeetestCardVerifier
private readonly CardClient cardClient;
private readonly HomaGeetestClient homaGeetestClient;
public async ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CancellationToken token)
public async ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CardVerifiationHeaders headers, CancellationToken token)
{
Response.Response<VerificationRegistration> registrationResponse = await cardClient.CreateVerificationAsync(user, token).ConfigureAwait(false);
Response.Response<VerificationRegistration> registrationResponse = await cardClient.CreateVerificationAsync(user, headers, token).ConfigureAwait(false);
if (registrationResponse.IsOk())
{
VerificationRegistration registration = registrationResponse.Data;
@@ -24,7 +24,7 @@ internal sealed partial class HomaGeetestCardVerifier : IGeetestCardVerifier
if (response is { Code: 0, Data.Validate: string validate })
{
Response.Response<VerificationResult> verifyResponse = await cardClient.VerifyVerificationAsync(registration.Challenge, validate, token).ConfigureAwait(false);
Response.Response<VerificationResult> verifyResponse = await cardClient.VerifyVerificationAsync(headers, registration.Challenge, validate, token).ConfigureAwait(false);
if (verifyResponse.IsOk())
{
VerificationResult result = verifyResponse.Data;

View File

@@ -7,5 +7,5 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Verification;
internal interface IGeetestCardVerifier
{
ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CancellationToken token);
ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CardVerifiationHeaders headers, CancellationToken token);
}

View File

@@ -9,8 +9,14 @@ namespace Snap.Hutao.Web.Response;
[HighQuality]
internal enum KnownReturnCode
{
/// <summary>
/// 用户不存在
/// </summary>
UserNotExist = -20001,
/// <summary>
/// 无效请求
/// 因战绩功能服务优化升级V2.10及以下版本将无法正常使用战绩功能,请更新米游社至最新版本再进行使用。
/// </summary>
InvalidRequest = -10001,
@@ -120,22 +126,32 @@ internal enum KnownReturnCode
LoginStateInvalid = 1004,
/// <summary>
/// 账号有风险
/// 实时便笺 账号有风险
/// </summary>
CODE1034 = 1034,
/// <summary>
/// 请登录
/// 实时便笺 当前账号存在风险,暂无数据
/// </summary>
CODE5003 = 5003,
/// <summary>
/// 请登录 登录后可查看战绩信息
/// </summary>
PleaseLogin = 10001,
/// <summary>
/// 原神战绩 查看他人战绩次数过多,请休息一会儿再试
/// </summary>
CODE10101 = 10101,
/// <summary>
/// 数据未公开
/// </summary>
DataIsNotPublicForTheUser = 10102,
/// <summary>
/// 实时便笺
/// 实时便笺 你的账号已被封禁,无法查看
/// </summary>
CODE10103 = 10103,
@@ -143,4 +159,4 @@ internal enum KnownReturnCode
/// 实时便笺
/// </summary>
CODE10104 = 10104,
}
}