diff --git a/README.md b/README.md index 95e0f88e..986abd1b 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ ![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg) -## 项目首页 +## 项目首页(文档) [![Deploy Docs](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml) -[https://hut.ao](https://hut.ao) +[HUT.AO](https://hut.ao) ## 安装 diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/ContentDialogBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/ContentDialogBehavior.cs deleted file mode 100644 index a37f31b5..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/ContentDialogBehavior.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using CommunityToolkit.WinUI.UI.Behaviors; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Shapes; - -namespace Snap.Hutao.Control.Behavior; - -/// -/// Make ContentDialog's SmokeLayerBackground dsiplay over custom titleBar -/// -public class ContentDialogBehavior : BehaviorBase -{ - /// - protected override void OnAssociatedObjectLoaded() - { - DependencyObject parent = VisualTreeHelper.GetParent(AssociatedObject); - - for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) - { - DependencyObject current = VisualTreeHelper.GetChild(parent, i); - if (current is Rectangle { Name: "SmokeLayerBackground" } background) - { - background.ClearValue(FrameworkElement.MarginProperty); - background.RegisterPropertyChangedCallback(FrameworkElement.MarginProperty, OnMarginChanged); - break; - } - } - } - - private static void OnMarginChanged(DependencyObject sender, DependencyProperty property) - { - if (property == FrameworkElement.MarginProperty) - { - sender.ClearValue(property); - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs index 5dbe10a9..2c679a97 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Extension/ContentDialogExtensions.cs @@ -3,8 +3,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.Xaml.Interactivity; -using Snap.Hutao.Control.Behavior; using Snap.Hutao.Core.Threading; namespace Snap.Hutao.Control.Extension; @@ -23,7 +21,6 @@ internal static class ContentDialogExtensions public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window) { contentDialog.XamlRoot = window.Content.XamlRoot; - Interaction.SetBehaviors(contentDialog, new() { new ContentDialogBehavior() }); return contentDialog; } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs index dc77c05c..a9ec0f49 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs @@ -121,19 +121,27 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control { await HideAsync(token); + LoadedImageSurface? imageSurface = null; + Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; + if (uri != null) { - StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri); - Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; - - LoadedImageSurface? imageSurface = null; - try + if (uri.Scheme == "ms-appx") { - imageSurface = await LoadImageSurfaceAsync(storageFile, token); + imageSurface = LoadedImageSurface.StartLoadFromUri(uri); } - catch (COMException) + else { - await imageCache.RemoveAsync(uri.Enumerate()); + StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri); + + try + { + imageSurface = await LoadImageSurfaceAsync(storageFile, token); + } + catch (COMException) + { + await imageCache.RemoveAsync(uri.Enumerate()); + } } if (imageSurface != null) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs index 353d6902..4e982a4b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs @@ -29,7 +29,6 @@ public class MonoChrome : CompositionImage { CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black); CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f); - CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay); CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush); diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs index 0a8d0a09..615854a3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs @@ -24,8 +24,8 @@ internal class I18NExtension : MarkupExtension static I18NExtension() { string currentName = CultureInfo.CurrentUICulture.Name; - Type languageType = TranslationMap.GetValueOrDefault(currentName, typeof(LanguagezhCN)); - Translation = (ITranslation)Activator.CreateInstance(languageType)!; + Type? languageType = ((IDictionary)TranslationMap).GetValueOrDefault(currentName, typeof(LanguagezhCN)); + Translation = (ITranslation)Activator.CreateInstance(languageType!)!; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs new file mode 100644 index 00000000..08e46afa --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs @@ -0,0 +1,32 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Markup; + +namespace Snap.Hutao.Control.Markup; + +/// +/// Uri扩展 +/// +[MarkupExtensionReturnType(ReturnType = typeof(Uri))] +public sealed class UriExtension : MarkupExtension +{ + /// + /// 构造一个新的Uri扩展 + /// + public UriExtension() + { + } + + /// + /// 地址 + /// + public string? Value { get; set; } + + /// + protected override object ProvideValue() + { + return new Uri(Value ?? string.Empty); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs index 91636fc1..6f660209 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs @@ -102,7 +102,7 @@ public class DescriptionTextBlock : ContentControl if (i == description.Length - 1) { - AppendText(text, description[last..i]); + AppendText(text, description[last..(i + 1)]); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index 50645d75..0b1fa3d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -22,7 +22,7 @@ internal static class CoreEnvironment /// /// 动态密钥2的盐 /// - public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk"; + public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"; /// /// 米游社请求UA diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs index 737dac63..432e17ea 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Json/Converter/StringEnumKeyDictionaryConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using System.Reflection; - namespace Snap.Hutao.Core.Json.Converter; /// @@ -33,7 +31,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory Type valueType = type.GetGenericArguments()[1]; Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType); - JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType, BindingFlags.Instance | BindingFlags.Public, null, new object[] { options }, null)!; + JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!; return converter; } @@ -41,13 +39,11 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory where TKey : struct, Enum { private readonly Type keyType; - private readonly Type valueType; - public StringEnumDictionaryConverterInner(JsonSerializerOptions options) + public StringEnumDictionaryConverterInner() { // Cache the key and value types. keyType = typeof(TKey); - valueType = typeof(TValue); } public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index 86741bb3..5f36760f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -88,6 +88,11 @@ internal static class EventIds /// 成就 /// public static readonly EventId Achievement = 100130; + + /// + /// 祈愿统计生成 + /// + public static readonly EventId GachaStatisticGeneration = 100140; #endregion #region 杂项 diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs index e4795d9b..b4367e5b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs @@ -99,6 +99,26 @@ public static partial class EnumerableExtensions return source.FirstOrDefault(predicate) ?? source.FirstOrDefault(); } + /// + /// 获取值或默认值 + /// + /// 键类型 + /// 值类型 + /// 字典 + /// 键 + /// 默认值 + /// 结果值 + public static TValue? GetValueOrDefault(this IDictionary dictionary, TKey key, TValue? defaultValue = default) + where TKey : notnull + { + if (dictionary.TryGetValue(key, out TValue? value)) + { + return value; + } + + return defaultValue; + } + /// /// 增加计数 /// @@ -111,6 +131,21 @@ public static partial class EnumerableExtensions ++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _); } + /// + /// 增加计数 + /// + /// 键类型 + /// 字典 + /// 键 + /// 增加的值 + public static void Increase(this Dictionary dict, TKey key, int value) + where TKey : notnull + { + // ref the value, so that we can manipulate it outside the dict. + ref int current = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _); + current += value; + } + /// /// 增加计数 /// @@ -166,6 +201,20 @@ public static partial class EnumerableExtensions return dictionary; } + /// + public static Dictionary ToDictionaryOverride(this IEnumerable source, Func keySelector, Func valueSelector) + where TKey : notnull + { + Dictionary dictionary = new(); + + foreach (TSource value in source) + { + dictionary[keySelector(value)] = valueSelector(value); + } + + return dictionary; + } + /// /// 表示一个对 类型的计数器 /// diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index 6cc4a0d2..c748caea 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -10,7 +10,6 @@ namespace Snap.Hutao; /// 主窗体 /// [Injection(InjectAs.Singleton)] -[Injection(InjectAs.Singleton, typeof(Window))] // This is for IEnumerable [SuppressMessage("", "CA1001")] public sealed partial class MainWindow : Window { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs new file mode 100644 index 00000000..d723a0dd --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs @@ -0,0 +1,67 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 角色信息 +/// +public class Avatar +{ + /// + /// 名称 + /// + public string Name { get; set; } = default!; + + /// + /// 图标 + /// + public Uri Icon { get; set; } = default!; + + /// + /// 侧面图标 + /// + public Uri SideIcon { get; set; } = default!; + + /// + /// 星级 + /// + public ItemQuality Quality { get; set; } + + /// + /// 等级 + /// + public string Level { get; set; } = default!; + + /// + /// 好感度等级 + /// + public int FetterLevel { get; set; } + + /// + /// 武器 + /// + public Weapon Weapon { get; set; } = default!; + + /// + /// 圣遗物列表 + /// + public List Reliquaries { get; set; } = default!; + + /// + /// 命之座列表 + /// + public List Constellations { get; set; } = default!; + + /// + /// 技能列表 + /// + public List Skills { get; set; } = default!; + + /// + /// 属性 + /// + public List> Properties { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Constellation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Constellation.cs new file mode 100644 index 00000000..3425ea83 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Constellation.cs @@ -0,0 +1,15 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 命座信息 +/// +public class Constellation : NameIconDescription +{ + /// + /// 是否激活 + /// + public bool IsActiviated { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/EquipBase.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/EquipBase.cs new file mode 100644 index 00000000..ae93b6be --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/EquipBase.cs @@ -0,0 +1,27 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 装备基类 +/// +public class EquipBase : NameIconDescription +{ + /// + /// 等级 + /// + public string Level { get; set; } = default!; + + /// + /// 品质 + /// + public ItemQuality Quality { get; set; } + + /// + /// 主属性 + /// + public Pair MainProperty { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/NameIconDescription.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/NameIconDescription.cs new file mode 100644 index 00000000..38cf8603 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/NameIconDescription.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 名称与描述抽象 +/// +public abstract class NameIconDescription +{ + /// + /// 名称 + /// + public string Name { get; set; } = default!; + + /// + /// 图标 + /// + public Uri Icon { get; set; } = default!; + + /// + /// 描述 + /// + public string Description { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs new file mode 100644 index 00000000..18fa5772 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs @@ -0,0 +1,40 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 玩家信息 +/// +public class Player +{ + /// + /// 昵称 + /// + public string Nickname { get; set; } = default!; + + /// + /// 等级 + /// + public int Level { get; set; } + + /// + /// 签名 + /// + public string Signature { get; set; } = default!; + + /// + /// 完成成就数 + /// + public int FinishAchievementNumber { get; set; } + + /// + /// 深渊层间 + /// + public string SipralAbyssFloorLevel { get; set; } = default!; + + /// + /// 头像 + /// + public Uri ProfilePicture { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs new file mode 100644 index 00000000..a4767c12 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs @@ -0,0 +1,15 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 圣遗物 +/// +public class Reliquary : EquipBase +{ + /// + /// 副属性列表 + /// + public List> SubProperties { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Skill.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Skill.cs new file mode 100644 index 00000000..0f52b165 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Skill.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata; + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 天赋 +/// +public class Skill : NameIconDescription +{ + /// + /// 技能属性 + /// + public LevelParam Info { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Summary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Summary.cs new file mode 100644 index 00000000..22f53ea2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Summary.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 玩家与角色列表的包装器 +/// +public class Summary +{ + /// + /// 玩家信息 + /// + public Player Player { get; set; } = default!; + + /// + /// 角色列表 + /// + public List Avatars { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Weapon.cs new file mode 100644 index 00000000..8079dc66 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Weapon.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 武器 +/// +public class Weapon : EquipBase +{ + /// + /// 副属性 + /// + public Pair SubProperty { get; set; } = default!; + + /// + /// 精炼属性 + /// + public string AffixLevel { get; set; } = default!; + + /// + /// 精炼名称 + /// + public string AffixName { get; set; } = default!; + + /// + /// 精炼被动 + /// + public string AffixDescription { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs index d33aa6b1..47af3cc4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/AvatarInfo.cs @@ -28,4 +28,15 @@ public class AvatarInfo /// 角色的信息 /// public Web.Enka.Model.AvatarInfo Info { get; set; } = default!; + + /// + /// 创建一个新的实体角色信息 + /// + /// uid + /// 角色信息 + /// 实体角色信息 + public static AvatarInfo Create(string uid, Web.Enka.Model.AvatarInfo info) + { + return new() { Uid = uid, Info = info }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs index 6b46e62d..fc1b49f8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/FightProperty.cs @@ -24,8 +24,10 @@ public enum FightProperty FIGHT_PROP_BASE_HP = 1, /// - /// 生命值加成 + /// 小生命值加成 /// + [Description("生命值")] + [Format(FormatMethod.Integer)] FIGHT_PROP_HP = 2, /// @@ -45,6 +47,8 @@ public enum FightProperty /// /// 攻击力加成 /// + [Description("攻击力")] + [Format(FormatMethod.Integer)] FIGHT_PROP_ATTACK = 5, /// @@ -64,6 +68,8 @@ public enum FightProperty /// /// 防御力加成 /// + [Description("防御力")] + [Format(FormatMethod.Integer)] FIGHT_PROP_DEFENSE = 8, /// @@ -387,6 +393,8 @@ public enum FightProperty /// /// 最大生命值 /// + [Description("生命值")] + [Format(FormatMethod.Integer)] FIGHT_PROP_MAX_HP = 2000, /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs index a415446c..61fe1241 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ItemType.cs @@ -15,7 +15,7 @@ public enum ItemType ITEM_NONE = 0, /// - /// 贵重道具 + /// 虚拟道具 /// ITEM_VIRTUAL = 1, diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs index a434f13b..49721a6e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Costume.cs @@ -27,4 +27,14 @@ public class Costume /// 是否为默认 /// public bool IsDefault { get; set; } + + /// + /// 图标 + /// + public string? Icon { get; set; } + + /// + /// 侧面图标 + /// + public string? SideIcon { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs index 8d5a151c..03d6f539 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/SkillDepot.cs @@ -24,7 +24,7 @@ public class SkillDepot public IList Inherents { get; set; } = default!; /// - /// 全部天赋 + /// 全部天赋,包括固有天赋 /// public IList CompositeSkills { @@ -36,6 +36,20 @@ public class SkillDepot /// public IList Talents { get; set; } = default!; + /// + /// 获取无固有天赋的技能列表 + /// + /// 天赋列表 + public IEnumerable GetCompositeSkillsNoInherents() + { + foreach (ProudableSkill skill in Skills) + { + yield return skill; + } + + yield return EnergySkill; + } + private IEnumerable GetCompositeSkills() { foreach (ProudableSkill skill in Skills) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs index 7b2f41be..753800d4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/DescParamDescriptor.cs @@ -12,22 +12,43 @@ namespace Snap.Hutao.Model.Metadata.Converter; /// internal sealed class DescParamDescriptor : ValueConverterBase>> { + /// + /// 获取特定等级的解释 + /// + /// 源 + /// 等级 + /// 特定等级的解释 + public static LevelParam Convert(DescParam from, int level) + { + // DO NOT INLINE! + // Cache the formats + IList formats = from.Descriptions + .Select(desc => new DescFormat(desc)) + .ToList(); + + LevelParam param = from.Parameters.Single(param => param.Level == level); + + return new LevelParam($"Lv.{param.Level}", GetParameterInfos(formats, param.Parameters)); + } + /// public override IList> Convert(DescParam from) { + // DO NOT INLINE! + // Cache the formats + IList formats = from.Descriptions + .Select(desc => new DescFormat(desc)) + .ToList(); + IList> parameters = from.Parameters - .Select(param => new LevelParam(param.Level.ToString(), GetParameterInfos(from, param.Parameters))) + .Select(param => new LevelParam(param.Level.ToString(), GetParameterInfos(formats, param.Parameters))) .ToList(); return parameters; } - private static IList GetParameterInfos(DescParam rawDescParam, IList param) + private static IList GetParameterInfos(IList formats, IList param) { - IList formats = rawDescParam.Descriptions - .Select(desc => new DescFormat(desc)) - .ToList(); - List results = new(); for (int index = 0; index < formats.Count; index++) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs index e5f7caf7..295ee201 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/EquipIconConverter.cs @@ -27,4 +27,4 @@ internal class EquipIconConverter : ValueConverterBase { return IconNameToUri(from); } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs index 7aad8d22..7969ab6f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs @@ -13,6 +13,25 @@ namespace Snap.Hutao.Model.Metadata.Converter; /// internal class PropertyInfoDescriptor : ValueConverterBase>?> { + /// + /// 格式化战斗属性 + /// + /// 战斗属性 + /// 值 + /// 格式化的值 + public static string FormatProperty(FightProperty property, double value) + { + FormatMethod method = property.GetFormatMethod(); + + string valueFormatted = method switch + { + FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(), + FormatMethod.Percent => string.Format("{0:P1}", value), + _ => value.ToString(), + }; + return valueFormatted; + } + /// public override IList> Convert(PropertyInfo from) { @@ -34,14 +53,7 @@ internal class PropertyInfoDescriptor : ValueConverterBase Math.Round((double)param, MidpointRounding.AwayFromZero).ToString(), - FormatMethod.Percent => string.Format("{0:P1}", param), - _ => param.ToString(), - }; + string valueFormatted = FormatProperty(properties[index], param); results.Add(new ParameterInfo { Description = properties[index].GetDescription(), Parameter = valueFormatted }); } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs new file mode 100644 index 00000000..ff014fbe --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Control; +using Snap.Hutao.Model.Intrinsic; +using Windows.UI; + +namespace Snap.Hutao.Model.Metadata.Converter; + +/// +/// 品质颜色转换器 +/// +internal class QualityColorConverter : ValueConverterBase +{ + /// + public override Color Convert(ItemQuality from) + { + return from switch + { + ItemQuality.QUALITY_WHITE => Color.FromArgb(0xFF, 0x72, 0x77, 0x8B), + ItemQuality.QUALITY_GREEN => Color.FromArgb(0xFF, 0x2A, 0x8F, 0x72), + ItemQuality.QUALITY_BLUE => Color.FromArgb(0xFF, 0x51, 0x80, 0xCB), + ItemQuality.QUALITY_PURPLE => Color.FromArgb(0xFF, 0xA1, 0x56, 0xE0), + ItemQuality.QUALITY_ORANGE or ItemQuality.QUALITY_ORANGE_SP => Color.FromArgb(0xFF, 0xBC, 0x69, 0x32), + _ => Color.FromArgb(0x00, 0x00, 0x00, 0x00), + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs new file mode 100644 index 00000000..4b33193e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/RelicIconConverter.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Control; + +namespace Snap.Hutao.Model.Metadata.Converter; + +/// +/// 武器图片转换器 +/// +internal class RelicIconConverter : ValueConverterBase +{ + private const string BaseUrl = "https://static.snapgenshin.com/RelicIcon/{0}.png"; + + /// + /// 名称转Uri + /// + /// 名称 + /// 链接 + public static Uri IconNameToUri(string name) + { + return new Uri(string.Format(BaseUrl, name)); + } + + /// + public override Uri Convert(string from) + { + return IconNameToUri(from); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs index 047e6513..7dc477bd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/SkillIconConverter.cs @@ -13,16 +13,26 @@ internal class SkillIconConverter : ValueConverterBase private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png"; private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png"; - /// - public override Uri Convert(string from) + /// + /// 名称转Uri + /// + /// 名称 + /// 链接 + public static Uri IconNameToUri(string name) { - if (from.StartsWith("UI_Talent_")) + if (name.StartsWith("UI_Talent_")) { - return new Uri(string.Format(TalentUrl, from)); + return new Uri(string.Format(TalentUrl, name)); } else { - return new Uri(string.Format(SkillUrl, from)); + return new Uri(string.Format(SkillUrl, name)); } } + + /// + public override Uri Convert(string from) + { + return IconNameToUri(from); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs index b6c65184..36f914f3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/Reliquary.cs @@ -18,7 +18,7 @@ public class Reliquary /// /// 允许出现的等级 /// - public IEnumerable RankLevels { get; set; } = default!; + public ItemQuality RankLevel { get; set; } = default!; /// /// 套装Id diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs index d4dd9c22..d37210ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffixBase.cs @@ -19,4 +19,4 @@ public class ReliquaryAffixBase /// 战斗属性 /// public FightProperty Type { get; set; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs new file mode 100644 index 00000000..8e9109ee --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryLevel.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Json.Converter; +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Model.Metadata.Reliquary; + +/// +/// 圣遗物等级 +/// +public class ReliquaryLevel +{ + /// + /// 品质 + /// + public ItemQuality Quality { get; set; } + + /// + /// 等级 1-21 + /// + public int Level { get; set; } + + /// + /// 属性 + /// + [JsonConverter(typeof(StringEnumKeyDictionaryConverter))] + public IDictionary Properties { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs new file mode 100644 index 00000000..df52d98e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Metadata.Weapon; + +/// +/// 武器被动信息 +/// +public class AffixInfo +{ + /// + /// 被动的名称 + /// + public string Name { get; set; } = default!; + + /// + /// 各个等级的描述 + /// 0-4 + /// + public List> Descriptions { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs new file mode 100644 index 00000000..5924230d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/LevelDescription.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Metadata.Weapon; + +/// +/// 等级与描述 +/// +/// 等级的类型 +public class LevelDescription +{ + /// + /// 等级 + /// + public TLevel Level { get; set; } = default!; + + /// + /// 描述 + /// + public string Description { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs index 46f613d0..de5fc9bd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs @@ -54,6 +54,11 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality /// public PropertyInfo Property { get; set; } = default!; + /// + /// 被动信息, 无被动的武器为 + /// + public AffixInfo? Affix { get; set; } = default!; + /// [JsonIgnore] public ItemQuality Quality => RankLevel; @@ -110,4 +115,4 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality IsUp = isUp, }; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs b/src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs new file mode 100644 index 00000000..c4715a70 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs @@ -0,0 +1,41 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model; + +/// +/// 对 +/// +/// 键的类型 +/// 值1的类型 +/// 值2的类型 +public class Pair2 +{ + /// + /// 构造一个新的对 + /// + /// 键 + /// 值1 + /// 值2 + public Pair2(TKey key, TValue1 value1, TValue2 value2) + { + Key = key; + Value1 = value1; + Value2 = value2; + } + + /// + /// 键 + /// + public TKey Key { get; set; } + + /// + /// 值 + /// + public TValue1 Value1 { get; set; } + + /// + /// 值 + /// + public TValue2 Value2 { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png new file mode 100644 index 00000000..d91d016f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png new file mode 100644 index 00000000..db6ba038 Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs new file mode 100644 index 00000000..98d9edee --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs @@ -0,0 +1,118 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Context.Database; +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Service.AvatarInfo.Factory; +using Snap.Hutao.Web.Enka; +using Snap.Hutao.Web.Enka.Model; +using Snap.Hutao.Web.Hoyolab; + +namespace Snap.Hutao.Service.AvatarInfo; + +/// +/// 角色信息服务 +/// +[Injection(InjectAs.Transient, typeof(IAvatarInfoService))] +internal class AvatarInfoService : IAvatarInfoService +{ + private readonly AppDbContext appDbContext; + private readonly ISummaryFactory summaryFactory; + private readonly EnkaClient enkaClient; + + /// + /// 构造一个新的角色信息服务 + /// + /// 数据库上下文 + /// 简述工厂 + /// Enka客户端 + public AvatarInfoService(AppDbContext appDbContext, ISummaryFactory summaryFactory, EnkaClient enkaClient) + { + this.appDbContext = appDbContext; + this.summaryFactory = summaryFactory; + this.enkaClient = enkaClient; + } + + /// + public async Task> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default) + { + if (HasOption(refreshOption, RefreshOption.RequestFromAPI)) + { + EnkaResponse? resp = await GetEnkaResponseAsync(uid, token).ConfigureAwait(false); + if (resp == null) + { + return new(RefreshResult.APIUnavailable, null); + } + + if (resp.IsValid) + { + IList list = HasOption(refreshOption, RefreshOption.StoreInDatabase) + ? UpdateDbAvatarInfo(uid.Value, resp.AvatarInfoList) + : resp.AvatarInfoList; + + Summary summary = await summaryFactory.CreateAsync(resp.PlayerInfo, list).ConfigureAwait(false); + return new(RefreshResult.Ok, summary); + } + else + { + return new(RefreshResult.ShowcaseNotOpen, null); + } + } + else + { + PlayerInfo info = PlayerInfo.CreateEmpty(uid.Value); + + Summary summary = await summaryFactory.CreateAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false); + return new(RefreshResult.Ok, summary); + } + } + + private static bool HasOption(RefreshOption source, RefreshOption define) + { + return (source & define) == define; + } + + private async Task GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default) + { + return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false) + ?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false); + } + + private List UpdateDbAvatarInfo(string uid, IEnumerable webInfos) + { + List dbInfos = appDbContext.AvatarInfos + .Where(i => i.Uid == uid) + .ToList(); + + foreach (Web.Enka.Model.AvatarInfo webInfo in webInfos) + { + Model.Entity.AvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId); + + if (entity == null) + { + entity = Model.Entity.AvatarInfo.Create(uid, webInfo); + appDbContext.Add(entity); + } + else + { + entity.Info = webInfo; + appDbContext.Update(entity); + } + } + + appDbContext.SaveChanges(); + + return GetDbAvatarInfos(uid); + } + + private List GetDbAvatarInfos(string uid) + { + return appDbContext.AvatarInfos + .Where(i => i.Uid == uid) + .Select(i => i.Info) + .AsEnumerable() + .OrderByDescending(i => i.AvatarId) + .ToList(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs new file mode 100644 index 00000000..d7c8d20b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ISummaryFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Web.Enka.Model; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 简述工厂 +/// +internal interface ISummaryFactory +{ + /// + /// 异步创建简述对象 + /// + /// 玩家信息 + /// 角色列表 + /// 简述对象 + Task CreateAsync(PlayerInfo playerInfo, IEnumerable avatarInfos); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs new file mode 100644 index 00000000..240b6e5c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs @@ -0,0 +1,196 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using Snap.Hutao.Model; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Metadata.Reliquary; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.Web.Enka.Model; +using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; +using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; +using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; +using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; +using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo; +using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.Avatar; +using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary; +using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.Weapon; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 简述工厂 +/// +[Injection(InjectAs.Transient, typeof(ISummaryFactory))] +internal class SummaryFactory : ISummaryFactory +{ + private readonly IMetadataService metadataService; + + /// + /// 构造一个新的简述工厂 + /// + /// 元数据服务 + public SummaryFactory(IMetadataService metadataService) + { + this.metadataService = metadataService; + } + + /// + public async Task CreateAsync(ModelPlayerInfo playerInfo, IEnumerable avatarInfos) + { + Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); + Dictionary idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false); + Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false); + Dictionary idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false); + + List reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false); + List reliquaries = await metadataService.GetReliquariesAsync().ConfigureAwait(false); + + SummaryFactoryInner inner = new(idAvatarMap, idRelicMainPropMap, idWeaponMap, idReliquaryAffixMap, reliqueryLevels, reliquaries); + return inner.Create(playerInfo, avatarInfos); + } + + private class SummaryFactoryInner + { + private readonly Dictionary idAvatarMap; + private readonly Dictionary idRelicMainPropMap; + private readonly Dictionary idWeaponMap; + private readonly Dictionary idReliquaryAffixMap; + private readonly List reliqueryLevels; + private readonly List reliquaries; + + public SummaryFactoryInner( + Dictionary idAvatarMap, + Dictionary idRelicMainPropMap, + Dictionary idWeaponMap, + Dictionary idReliquaryAffixMap, + List reliqueryLevels, + List reliquaries) + { + this.idAvatarMap = idAvatarMap; + this.idRelicMainPropMap = idRelicMainPropMap; + this.idWeaponMap = idWeaponMap; + this.reliqueryLevels = reliqueryLevels; + this.reliquaries = reliquaries; + this.idReliquaryAffixMap = idReliquaryAffixMap; + } + + public Summary Create(ModelPlayerInfo playerInfo, IEnumerable avatarInfos) + { + // 用作头像 + MetadataAvatar avatar = idAvatarMap[playerInfo.ProfilePicture.AvatarId]; + + return new() + { + Player = SummaryHelper.CreatePlayer(playerInfo, avatar), + Avatars = avatarInfos.Select(a => CreateAvatar(a)).ToList(), + }; + } + + private PropertyAvatar CreateAvatar(ModelAvatarInfo avatarInfo) + { + (List reliquaries, PropertyWeapon weapon) = ProcessEquip(avatarInfo.EquipList); + MetadataAvatar avatar = idAvatarMap[avatarInfo.AvatarId]; + + return new() + { + Name = avatar.Name, + Icon = AvatarIconConverter.IconNameToUri(avatar.Icon), + SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon), + Quality = avatar.Quality, + Level = avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty, + FetterLevel = avatarInfo.FetterInfo.ExpLevel, + Weapon = weapon, + Reliquaries = reliquaries, + Constellations = SummaryHelper.CreateConstellations(avatarInfo.TalentIdList, avatar.SkillDepot.Talents), + Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.GetCompositeSkillsNoInherents()), + Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap), + }; + } + + private (List Reliquaries, PropertyWeapon Weapon) ProcessEquip(IList equipments) + { + List reliquaries = new(); + PropertyWeapon? weapon = null; + + foreach (Equip equip in equipments) + { + switch (equip.Flat.ItemType) + { + case ItemType.ITEM_RELIQUARY: + reliquaries.Add(CreateReliquary(equip)); + break; + case ItemType.ITEM_WEAPON: + weapon = CreateWeapon(equip); + break; + } + } + + return (reliquaries, weapon!); + } + + private PropertyReliquary CreateReliquary(Equip equip) + { + MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId)); + + return new() + { + // NameIconDescription + Name = reliquary.Name, + Icon = RelicIconConverter.IconNameToUri(reliquary.Icon), + Description = reliquary.Description, + + // EquipBase + Level = $"+{equip.Reliquary!.Level - 1}", + Quality = reliquary.RankLevel, + MainProperty = CreateReliquaryMainProperty(equip.Reliquary.MainPropId, reliquary.RankLevel, equip.Reliquary.Level), + + // Reliquary + SubProperties = equip.Reliquary.AppendPropIdList.Select(id => CreateReliquarySubProperty(id)).ToList(), + }; + } + + private Pair CreateReliquaryMainProperty(int propId, ItemQuality quality, int level) + { + ReliquaryLevel reliquaryLevel = reliqueryLevels.Single(r => r.Level == level && r.Quality == quality); + FightProperty property = idRelicMainPropMap[propId]; + + return new(property.GetDescription(), PropertyInfoDescriptor.FormatProperty(property, reliquaryLevel.Properties[property])); + } + + private Pair CreateReliquarySubProperty(int appendPropId) + { + ReliquaryAffix affix = idReliquaryAffixMap[appendPropId]; + FightProperty property = affix.Type; + + return new(property.GetDescription(), PropertyInfoDescriptor.FormatProperty(property, affix.Value)); + } + + private PropertyWeapon CreateWeapon(Equip equip) + { + MetadataWeapon weapon = idWeaponMap[equip.ItemId]; + (string id, int level) = equip.Weapon!.AffixMap.Single(); + + return new() + { + // NameIconDescription + Name = weapon.Name, + Icon = EquipIconConverter.IconNameToUri(weapon.Icon), + Description = weapon.Description, + + // EquipBase + Level = $"Lv.{equip.Weapon!.Level}", + Quality = weapon.Quality, + MainProperty = new(string.Empty, string.Empty), // TODO + + // Weapon + SubProperty = new(string.Empty, string.Empty), // TODO + AffixLevel = $"精炼{level + 1}", + AffixName = weapon.Affix?.Name ?? string.Empty, + AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == level).Description ?? string.Empty, + }; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs new file mode 100644 index 00000000..4bf21ff3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs @@ -0,0 +1,227 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using Snap.Hutao.Model; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Annotation; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Web.Enka.Model; +using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; +using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 简述帮助类 +/// +internal static class SummaryHelper +{ + /// + /// 创建玩家对象 + /// + /// 玩家信息 + /// 角色 + /// 玩家对象 + public static Player CreatePlayer(ModelPlayerInfo playerInfo, MetadataAvatar avatar) + { + return new() + { + Nickname = playerInfo.Nickname, + Level = playerInfo.Level, + Signature = playerInfo.Signature, + FinishAchievementNumber = playerInfo.FinishAchievementNum, + SipralAbyssFloorLevel = $"{playerInfo.TowerFloorIndex} - {playerInfo.TowerLevelIndex}", + ProfilePicture = AvatarIconConverter.IconNameToUri(GetIconName(playerInfo.ProfilePicture, avatar)), + }; + } + + /// + /// 创建命之座 + /// + /// 激活的命座列表 + /// 全部命座 + /// 命之座 + public static List CreateConstellations(IList? talentIds, IList talents) + { + List constellations = new(); + + foreach (SkillBase talent in talents) + { + Constellation constellation = new() + { + Name = talent.Name, + Icon = SkillIconConverter.IconNameToUri(talent.Icon), + Description = talent.Description, + IsActiviated = talentIds?.Contains(talent.Id) ?? false, + }; + + constellations.Add(constellation); + } + + return constellations; + } + + /// + /// 创建技能组 + /// + /// 技能等级映射 + /// 额外提升等级映射 + /// 技能列表 + /// 技能 + public static List CreateSkills(IDictionary skillLevelMap, IDictionary? proudSkillExtraLevelMap, IEnumerable proudSkills) + { + Dictionary skillLevelMapCopy = new(skillLevelMap); + + if (proudSkillExtraLevelMap != null) + { + foreach ((string skillGroupId, int extraLevel) in proudSkillExtraLevelMap) + { + int skillGroupIdInt32 = int.Parse(skillGroupId); + int skillId = proudSkills.Single(p => p.GroupId == skillGroupIdInt32).Id; + + skillLevelMapCopy.Increase($"{skillId}", extraLevel); + } + } + + List skills = new(); + + foreach (ProudableSkill proudableSkill in proudSkills) + { + Skill skill = new() + { + Name = proudableSkill.Name, + Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon), + Description = proudableSkill.Description, + Info = DescParamDescriptor.Convert(proudableSkill.Proud, skillLevelMapCopy[$"{proudableSkill.Id}"]), + }; + + skills.Add(skill); + } + + return skills; + } + + /// + /// 创建角色属性 + /// + /// 属性映射 + /// 列表 + public static List> CreateAvatarProperties(IDictionary fightPropMap) + { + List> properties; + + double baseHp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_HP); // 1 + double hp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP); // 2 + double hpPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP_PERCENT); // 3 + double hpAdd = hp + (baseHp * hpPercent); + double maxHp = baseHp + hpAdd; + Pair2 hpPair2 = new("生命值", FormatValue(FormatMethod.Integer, maxHp), $"[+{FormatValue(FormatMethod.Integer, hpAdd)}]"); + + double baseAtk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_ATTACK); // 4 + double atk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK); // 5 + double atkPrecent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6 + double atkAdd = atk + (baseAtk * atkPrecent); + double maxAtk = baseAtk + atkAdd; + Pair2 atkPair2 = new("攻击力", FormatValue(FormatMethod.Integer, maxAtk), $"[+{FormatValue(FormatMethod.Integer, atkAdd)}]"); + + double baseDef = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_DEFENSE); // 7 + double def = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE); // 8 + double defPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9 + double defAdd = def + (baseDef * defPercent); + double maxDef = baseDef + defPercent; + Pair2 defPair2 = new("防御力", FormatValue(FormatMethod.Integer, maxDef), $"[+{FormatValue(FormatMethod.Integer, defAdd)}]"); + + double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28 + Pair2 emPair2 = new("元素精通", FormatValue(FormatMethod.Integer, em), null); + + double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20 + Pair2 critRatePair2 = new("暴击率", FormatValue(FormatMethod.Percent, critRate), null); + + double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22 + Pair2 critDMGPair2 = new("暴击伤害", FormatValue(FormatMethod.Percent, critDMG), null); + + double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23 + Pair2 chargeEffPair2 = new("元素充能效率", FormatValue(FormatMethod.Percent, chargeEff), null); + + properties = new() { hpPair2, atkPair2, defPair2, emPair2, critRatePair2, critDMGPair2, chargeEffPair2 }; + + FightProperty bonusProperty = GetBonusFightProperty(fightPropMap); + if (bonusProperty != FightProperty.FIGHT_PROP_NONE) + { + double value = fightPropMap[bonusProperty]; + Pair2 bonusPair2 = new(bonusProperty.GetDescription(), FormatValue(FormatMethod.Percent, value), null); + properties.Add(bonusPair2); + } + + return properties; + } + + private static string FormatValue(FormatMethod method, double value) + { + return method switch + { + FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(), + FormatMethod.Percent => string.Format("{0:P1}", value), + _ => value.ToString(), + }; + } + + private static FightProperty GetBonusFightProperty(IDictionary fightPropMap) + { + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY)) + { + return FightProperty.FIGHT_PROP_FIRE_ADD_HURT; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY)) + { + return FightProperty.FIGHT_PROP_ELEC_ADD_HURT; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WATER_ENERGY)) + { + return FightProperty.FIGHT_PROP_WATER_ADD_HURT; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY)) + { + return FightProperty.FIGHT_PROP_GRASS_ADD_HURT; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_WIND_ENERGY)) + { + return FightProperty.FIGHT_PROP_WIND_ADD_HURT; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ICE_ENERGY)) + { + return FightProperty.FIGHT_PROP_ICE_ADD_HURT; + } + + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY)) + { + return FightProperty.FIGHT_PROP_ROCK_ADD_HURT; + } + + // 物伤 + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT)) + { + return FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT; + } + + return FightProperty.FIGHT_PROP_NONE; + } + + private static string GetIconName(ProfilePicture profilePicture, MetadataAvatar avatar) + { + if (profilePicture.CostumeId != null) + { + return avatar.Costumes.Single(c => c.Id == profilePicture.CostumeId).Icon ?? string.Empty; + } + + return avatar.Icon; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs new file mode 100644 index 00000000..8252f91d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/IAvatarInfoService.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Web.Hoyolab; + +namespace Snap.Hutao.Service.AvatarInfo; + +/// +/// 角色信息服务 +/// +internal interface IAvatarInfoService +{ + /// + /// 异步获取总览数据 + /// + /// uid + /// 刷新选项 + /// 取消令牌 + /// 总览数据 + Task> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshOption.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshOption.cs new file mode 100644 index 00000000..65bd424f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshOption.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.AvatarInfo; + +/// +/// 刷新选项 +/// +[Flags] +public enum RefreshOption +{ + /// + /// 是否存入本地数据库 + /// + StoreInDatabase = 0b00000001, + + /// + /// 从API获取 + /// + RequestFromAPI = 0b00000010, + + /// + /// 仅数据库 + /// + DatabaseOnly = 0b00000000, + + /// + /// 标准操作 + /// + Standard = StoreInDatabase | RequestFromAPI, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs new file mode 100644 index 00000000..d498383b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/RefreshResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.AvatarInfo; + +/// +/// 刷新结果 +/// +public enum RefreshResult +{ + /// + /// 正常 + /// + Ok, + + /// + /// API 不可用 + /// + APIUnavailable, + + /// + /// 角色橱窗未对外开放 + /// + ShowcaseNotOpen, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs index af31d25e..40c4453e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -5,6 +5,8 @@ using CommunityToolkit.Mvvm.Messaging; using Snap.Hutao.Context.Database; using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Threading; using Snap.Hutao.Extension; using Snap.Hutao.Model.Binding.Gacha; @@ -45,6 +47,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization private readonly IMetadataService metadataService; private readonly IInfoBarService infoBarService; private readonly IGachaStatisticsFactory gachaStatisticsFactory; + private readonly ILogger logger; private readonly DbCurrent dbCurrent; private readonly Dictionary itemBaseCache = new(); @@ -65,6 +68,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization /// 元数据服务 /// 信息条服务 /// 祈愿统计工厂 + /// 日志器 /// 消息器 public GachaLogService( AppDbContext appDbContext, @@ -73,6 +77,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization IMetadataService metadataService, IInfoBarService infoBarService, IGachaStatisticsFactory gachaStatisticsFactory, + ILogger logger, IMessenger messenger) { this.appDbContext = appDbContext; @@ -80,6 +85,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization this.gachaInfoClient = gachaInfoClient; this.metadataService = metadataService; this.infoBarService = infoBarService; + this.logger = logger; this.gachaStatisticsFactory = gachaStatisticsFactory; dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger); @@ -143,21 +149,24 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization } /// - public Task GetStatisticsAsync(GachaArchive? archive = null) + public async Task GetStatisticsAsync(GachaArchive? archive = null) { archive ??= CurrentArchive; // Return statistics if (archive != null) { + ValueStopwatch stopwatch = ValueStopwatch.StartNew(); IQueryable items = appDbContext.GachaItems .Where(i => i.ArchiveId == archive.InnerId); - return gachaStatisticsFactory.CreateAsync(items); + GachaStatistics statistics = await gachaStatisticsFactory.CreateAsync(items).ConfigureAwait(false); + logger.LogInformation(EventIds.GachaStatisticGeneration, "GachaStatistic Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds); + return statistics; } else { - return Must.Fault("没有选中的存档"); + throw Must.NeverHappen(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs index 8ed56937..9109577b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/IMetadataService.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Avatar; @@ -47,21 +48,35 @@ internal interface IMetadataService /// /// 取消令牌 /// 卡池配置列表 - ValueTask> GetGachaEventsAsync(CancellationToken token = default(CancellationToken)); + ValueTask> GetGachaEventsAsync(CancellationToken token = default); /// /// 异步获取Id到角色的字典 /// /// 取消令牌 /// Id到角色的字典 - ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default(CancellationToken)); + ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default); + + /// + /// 异步获取ID到圣遗物副词条的字典 + /// + /// 取消令牌 + /// 字典 + ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default); + + /// + /// 异步获取圣遗物主词条Id与属性的字典 + /// + /// 取消令牌 + /// 字典 + ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default); /// /// 异步获取ID到武器的字典 /// /// 取消令牌 /// Id到武器的字典 - ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default(CancellationToken)); + ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default); /// /// 异步获取名称到角色的字典 @@ -91,6 +106,13 @@ internal interface IMetadataService /// 圣遗物强化属性列表 ValueTask> GetReliquaryAffixesAsync(CancellationToken token = default); + /// + /// 异步获取圣遗物等级数据 + /// + /// 取消令牌 + /// 圣遗物等级数据 + ValueTask> GetReliquaryLevelsAsync(CancellationToken token = default); + /// /// 异步获取圣遗物主属性强化属性列表 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index d6ebac6b..bf47506f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -8,6 +8,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Logging; using Snap.Hutao.Extension; +using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Avatar; @@ -122,6 +123,18 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor return FromCacheAsDictionaryAsync("Avatar", a => a.Id, token); } + /// + public ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("ReliquaryAffix", a => a.Id, token); + } + + /// + public ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("ReliquaryMainAffix", r => r.Id, r => r.Type, token); + } + /// public ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default) { @@ -152,6 +165,12 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor return FromCacheOrFileAsync>("ReliquaryAffix", token); } + /// + public ValueTask> GetReliquaryLevelsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("ReliquaryMainAffixLevel", token); + } + /// public ValueTask> GetReliquaryMainAffixesAsync(CancellationToken token = default) { @@ -295,4 +314,20 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor Dictionary dict = list.ToDictionaryOverride(keySelector); return memoryCache.Set(cacheKey, dict); } + + private async ValueTask> FromCacheAsDictionaryAsync(string fileName, Func keySelector, Func valueSelector, CancellationToken token) + where TKey : notnull + { + Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败"); + string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}.{typeof(TValue).Name}"; + + if (memoryCache.TryGetValue(cacheKey, out object? value)) + { + return Must.NotNull((Dictionary)value!); + } + + List list = await FromCacheOrFileAsync>(fileName, token).ConfigureAwait(false); + Dictionary dict = list.ToDictionaryOverride(keySelector, valueSelector); + return memoryCache.Set(cacheKey, dict); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 17851f76..98e200cc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -38,6 +38,8 @@ + + @@ -48,6 +50,7 @@ + @@ -56,6 +59,7 @@ + @@ -83,6 +87,8 @@ + + @@ -128,6 +134,16 @@ + + + MSBuild:Compile + + + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml index 86378265..206e9a65 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml @@ -1,9 +1,8 @@  - - - - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml new file mode 100644 index 00000000..5b66372b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml @@ -0,0 +1,22 @@ + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml.cs new file mode 100644 index 00000000..b6df6a3f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AvatarInfoQueryDialog.xaml.cs @@ -0,0 +1,47 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Web.Hoyolab; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 角色信息查询UID对话框 +/// +public sealed partial class AvatarInfoQueryDialog : ContentDialog +{ + /// + /// 构造一个新的角色信息查询UID对话框 + /// + /// 窗口 + public AvatarInfoQueryDialog(Window window) + { + InitializeComponent(); + XamlRoot = window.Content.XamlRoot; + } + + /// + /// 获取玩家UID + /// + /// 玩家UID + public async Task> GetPlayerUidAsync() + { + ContentDialogResult result = await ShowAsync(); + + return new(result == ContentDialogResult.Primary, result == ContentDialogResult.Primary ? new(InputText.Text) : default); + } + + private void InputTextChanged(object sender, TextChangedEventArgs e) + { + bool inputValid = string.IsNullOrEmpty(InputText.Text) && InputText.Text.Length == 9; + + (PrimaryButtonText, IsPrimaryButtonEnabled) = inputValid switch + { + true => ("请输入正确的UID", false), + false => ("确认", true), + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml index 23f30928..5f34f208 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml @@ -5,18 +5,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls" - xmlns:mxi="using:Microsoft.Xaml.Interactivity" - xmlns:shcb="using:Snap.Hutao.Control.Behavior" mc:Ignorable="d" Title="导入祈愿记录" DefaultButton="Primary" PrimaryButtonText="确认" CloseButtonText="取消" Style="{StaticResource DefaultContentDialogStyle}"> - - - - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml index 839e3c57..35c85b3a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml @@ -7,12 +7,10 @@ xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls" xmlns:cwuconv="using:CommunityToolkit.WinUI.UI.Converters" xmlns:shvc="using:Snap.Hutao.View.Control" - xmlns:mxi="using:Microsoft.Xaml.Interactivity" - xmlns:shcb="using:Snap.Hutao.Control.Behavior" mc:Ignorable="d" Style="{StaticResource DefaultContentDialogStyle}" Title="获取祈愿物品中"> - + @@ -27,10 +25,6 @@ - - - - - - - - - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml index 5a393837..42f837ca 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml @@ -22,7 +22,7 @@ IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}"> @@ -34,14 +34,19 @@ Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_Gacha.png}"/> + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index e2b64d9a..b6cfd7ee 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -4,14 +4,15 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters" + xmlns:mxic="using:Microsoft.Xaml.Interactions.Core" xmlns:mxi="using:Microsoft.Xaml.Interactivity" - xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shc="using:Snap.Hutao.Control" + xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shci="using:Snap.Hutao.Control.Image" + xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter" xmlns:shv="using:Snap.Hutao.ViewModel" - xmlns:shcm="using:Snap.Hutao.Control.Markup" - xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" d:DataContext="{d:DesignInstance shv:AchievementViewModel}"> @@ -42,12 +43,23 @@ DefaultLabelPosition="Right"> + VerticalContentAlignment="Center" + Style="{StaticResource DefaultAutoSuggestBoxStyle}" + QueryIcon="{shcm:FontIcon Glyph=}"> + + + + + + @@ -134,7 +146,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml new file mode 100644 index 00000000..215da3b8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml @@ -0,0 +1,492 @@ + + + + + + + + + + 1 + + + 0.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml.cs new file mode 100644 index 00000000..b7c5b05c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Control; +using Snap.Hutao.ViewModel; + +namespace Snap.Hutao.View.Page; + +/// +/// 角色属性页 +/// +public sealed partial class AvatarPropertyPage : ScopedPage +{ + /// + /// 初始化一个新的角色属性页 + /// + public AvatarPropertyPage() + { + InitializeWith(); + InitializeComponent(); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index da331f14..1fcf46c2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -23,7 +23,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml index 6d4f0db1..927b5df5 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml @@ -73,10 +73,8 @@ - - - + OpenPaneLength="200" + PaneBackground="{StaticResource CardBackgroundFillColorSecondary}"> diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index 3f6f2da3..0bcfe77c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -197,15 +197,15 @@ + Text="Cookie操作"/> diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs index b40dd36f..62598222 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AchievementViewModel.cs @@ -57,6 +57,7 @@ internal class AchievementViewModel private ObservableCollection? archives; private Model.Entity.AchievementArchive? selectedArchive; private bool isIncompletedItemsFirst = true; + private string searchText = string.Empty; /// /// 构造一个新的成就视图模型 @@ -88,6 +89,7 @@ internal class AchievementViewModel ImportUIAFFromFileCommand = asyncRelayCommandFactory.Create(ImportUIAFFromFileAsync); AddArchiveCommand = asyncRelayCommandFactory.Create(AddArchiveAsync); RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync); + SearchAchievementCommand = new RelayCommand(SearchAchievement); SortIncompletedSwitchCommand = new RelayCommand(UpdateAchievementsSort); messenger.Register(this); @@ -147,10 +149,20 @@ internal class AchievementViewModel set { SetProperty(ref selectedAchievementGoal, value); + SearchText = string.Empty; UpdateAchievementFilter(value); } } + /// + /// 搜索文本 + /// + public string SearchText + { + get => searchText; + set => SetProperty(ref searchText, value); + } + /// /// 未完成优先 /// @@ -175,6 +187,11 @@ internal class AchievementViewModel /// public ICommand RemoveArchiveCommand { get; } + /// + /// 搜索成就命令 + /// + public ICommand SearchAchievementCommand { get; } + /// /// 从剪贴板导入UIAF命令 /// @@ -345,6 +362,30 @@ internal class AchievementViewModel } } + private void SearchAchievement(string? search) + { + if (Achievements != null) + { + SetProperty(ref selectedAchievementGoal, null); + + if (!string.IsNullOrEmpty(search)) + { + if (search.Length == 5 && int.TryParse(search, out int achiId)) + { + Achievements.Filter = (object o) => ((Model.Binding.Achievement)o).Inner.Id == achiId; + } + else + { + Achievements.Filter = (object o) => + { + Model.Binding.Achievement achi = (Model.Binding.Achievement)o; + return achi.Inner.Title.Contains(search) || achi.Inner.Description.Contains(search); + }; + } + } + } + } + [ThreadAccess(ThreadAccessState.AnyThread)] private async Task ImportUIAFFromClipboardAsync() { @@ -469,7 +510,7 @@ internal class AchievementViewModel { Achievements.Filter = goal != null ? ((object o) => o is Model.Binding.Achievement achi && achi.Inner.Goal == goal.Id) - : ((object o) => true); + : null; } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs new file mode 100644 index 00000000..aef9c5c6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs @@ -0,0 +1,148 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; +using Snap.Hutao.Control; +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Factory.Abstraction; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.AvatarInfo; +using Snap.Hutao.Service.User; +using Snap.Hutao.View.Dialog; +using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.Takumi.Binding; + +namespace Snap.Hutao.ViewModel; + +/// +/// 角色属性视图模型 +/// +[Injection(InjectAs.Transient)] +internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation +{ + private readonly IUserService userService; + private readonly IAvatarInfoService avatarInfoService; + private readonly IInfoBarService infoBarService; + private Summary? summary; + private Avatar? selectedAvatar; + + /// + /// 构造一个新的角色属性视图模型 + /// + /// 用户服务 + /// 角色信息服务 + /// 异步命令工厂 + /// 信息条服务 + public AvatarPropertyViewModel( + IUserService userService, + IAvatarInfoService avatarInfoService, + IAsyncRelayCommandFactory asyncRelayCommandFactory, + IInfoBarService infoBarService) + { + this.userService = userService; + this.avatarInfoService = avatarInfoService; + this.infoBarService = infoBarService; + + OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync); + RefreshByUserGameRoleCommand = asyncRelayCommandFactory.Create(RefreshByUserGameRoleAsync); + RefreshByInputUidCommand = asyncRelayCommandFactory.Create(RefreshByInputUidAsync); + } + + /// + public CancellationToken CancellationToken { get; set; } + + /// + /// 简述对象 + /// + public Summary? Summary { get => summary; set => SetProperty(ref summary, value); } + + /// + /// 选中的角色 + /// + public Avatar? SelectedAvatar { get => selectedAvatar; set => SetProperty(ref selectedAvatar, value); } + + /// + /// 加载界面命令 + /// + public ICommand OpenUICommand { get; set; } + + /// + /// 按当前角色刷新命令 + /// + public ICommand RefreshByUserGameRoleCommand { get; set; } + + /// + /// 按UID刷新命令 + /// + public ICommand RefreshByInputUidCommand { get; set; } + + private Task OpenUIAsync() + { + return RefreshCoreAsync(RefreshOption.DatabaseOnly); + } + + private Task RefreshByUserGameRoleAsync() + { + return RefreshCoreAsync(RefreshOption.Standard); + } + + private async Task RefreshByInputUidAsync() + { + MainWindow mainWindow = Ioc.Default.GetRequiredService(); + (bool isOk, PlayerUid uid) = await new AvatarInfoQueryDialog(mainWindow).GetPlayerUidAsync().ConfigureAwait(false); + + if (isOk) + { + (RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, RefreshOption.RequestFromAPI).ConfigureAwait(false); + + if (result == RefreshResult.Ok) + { + await ThreadHelper.SwitchToMainThreadAsync(); + Summary = summary; + } + else + { + switch (result) + { + case RefreshResult.APIUnavailable: + infoBarService.Warning("面板服务当前不可用"); + break; + case RefreshResult.ShowcaseNotOpen: + infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试"); + break; + } + } + } + } + + private async Task RefreshCoreAsync(RefreshOption option) + { + if (userService.Current is Model.Binding.User user) + { + if (user.SelectedUserGameRole is UserGameRole role) + { + (RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync((PlayerUid)role, option).ConfigureAwait(false); + + if (result == RefreshResult.Ok) + { + await ThreadHelper.SwitchToMainThreadAsync(); + Summary = summary; + SelectedAvatar = Summary?.Avatars.FirstOrDefault(); + } + else + { + switch (result) + { + case RefreshResult.APIUnavailable: + infoBarService.Warning("面板服务当前不可用"); + break; + case RefreshResult.ShowcaseNotOpen: + infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试"); + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs index bd664ff8..4e029e91 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs @@ -5,7 +5,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Web.Enka.Model; using Snap.Hutao.Web.Hoyolab; using System.Net.Http; -using System.Net.Http.Json; namespace Snap.Hutao.Web.Enka; @@ -19,14 +18,20 @@ internal class EnkaClient private const string EnkaAPIHutaoForward = "https://enka-api.hut.ao/{0}"; private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + private readonly ILogger logger; /// /// 构造一个新的 Enka API 客户端 /// /// http客户端 - public EnkaClient(HttpClient httpClient) + /// 序列化选项 + /// 日志器 + public EnkaClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger) { this.httpClient = httpClient; + this.options = options; + this.logger = logger; } /// @@ -37,7 +42,7 @@ internal class EnkaClient /// Enka API 响应 public Task GetForwardDataAsync(PlayerUid playerUid, CancellationToken token) { - return httpClient.GetFromJsonAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), token); + return httpClient.TryCatchGetFromJsonAsync(string.Format(EnkaAPIHutaoForward, playerUid.Value), options, logger, token); } /// @@ -48,6 +53,6 @@ internal class EnkaClient /// Enka API 响应 public Task GetDataAsync(PlayerUid playerUid, CancellationToken token) { - return httpClient.GetFromJsonAsync(string.Format(EnkaAPI, playerUid.Value), token); + return httpClient.TryCatchGetFromJsonAsync(string.Format(EnkaAPI, playerUid.Value), options, logger, token); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs index 28cd20b2..bb7c9457 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/AvatarInfo.cs @@ -31,7 +31,7 @@ public class AvatarInfo /// 命座 Id /// [JsonPropertyName("talentIdList")] - public IList TalentIdList { get; set; } = default!; + public IList? TalentIdList { get; set; } /// /// 属性Map @@ -86,5 +86,5 @@ public class AvatarInfo /// 命座额外技能等级 /// [JsonPropertyName("proudSkillExtraLevelMap")] - public IDictionary ProudSkillExtraLevelMap { get; set; } = default!; + public IDictionary? ProudSkillExtraLevelMap { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs index b90ff6d6..b44aba40 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/EnkaResponse.cs @@ -32,14 +32,7 @@ public class EnkaResponse /// public bool IsValid { - get => Ttl.HasValue; - } - - /// - /// 是否包含角色详细数据 - /// - public bool HasDetail - { - get => AvatarInfoList != null; + [MemberNotNullWhen(true, nameof(PlayerInfo), nameof(AvatarInfoList))] + get => PlayerInfo != null && AvatarInfoList != null; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs index 7f6f8903..f70a6463 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/PlayerInfo.cs @@ -83,4 +83,19 @@ public class PlayerInfo /// [JsonPropertyName("profilePicture")] public ProfilePicture ProfilePicture { get; set; } = default!; + + /// + /// 创建空对象 + /// + /// uid + /// 空的玩家信息 + public static PlayerInfo CreateEmpty(string uid) + { + return new() + { + Nickname = uid, + Signature = string.Empty, + ProfilePicture = new() { AvatarId = 10000046 }, // use Hutao as default. + }; + } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs index dd64788b..a32f6873 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/Model/ProfilePicture.cs @@ -13,4 +13,10 @@ public class ProfilePicture /// [JsonPropertyName("avatarId")] public int AvatarId { get; set; } + + /// + /// 衣装Id + /// + [JsonPropertyName("costumeId")] + public int? CostumeId { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs index d80bc00b..0df5901e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Web.WebView2.Core; using Snap.Hutao.Extension; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs index 18f71349..ccc5458d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider2.cs @@ -29,7 +29,7 @@ internal abstract class DynamicSecretProvider2 : Md5Convert string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options); // query - string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x)); + string q = string.Join('&', new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x)); // check string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant(); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs deleted file mode 100644 index c4bd216f..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab; - -/// -/// Uid Token 对 -/// -public struct UidToken -{ - /// - /// Uid - /// - public string Uid; - - /// - /// Token - /// - public string Token; - - /// - /// 构造一个新的 Uid Token 对 - /// - /// uid - /// token - public UidToken(string uid, string token) - { - Uid = uid; - Token = token; - } -} \ No newline at end of file