mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76800de6ee | ||
|
|
72b660119f | ||
|
|
67a1d5dc74 | ||
|
|
6e6d125814 | ||
|
|
55f16a6357 | ||
|
|
560068ca20 | ||
|
|
2f1981108e | ||
|
|
b545c0d09b | ||
|
|
009feced08 | ||
|
|
fa7fcbc9cc | ||
|
|
d28624dea1 | ||
|
|
d802d1af15 | ||
|
|
43e3df9cba | ||
|
|
8e5e59ad0d | ||
|
|
ed7d55ddd5 | ||
|
|
94ef94a621 | ||
|
|
fd35213741 | ||
|
|
0f752129b7 |
44
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
44
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -24,8 +24,8 @@ body:
|
||||
id: shver
|
||||
attributes:
|
||||
label: Snap Hutao 版本
|
||||
description: 在应用程序的设置界面中靠下的位置可以找到
|
||||
placeholder: 例:1.0.30.0
|
||||
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
|
||||
placeholder: 例:1.1.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -48,6 +48,42 @@ body:
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 相关的崩溃日志 位于 %HOMEPATH%/Documents/Hutao/Log.db
|
||||
description: 如果应用程序崩溃了,可以将崩溃日志复制后粘贴在此处,文件包含了敏感信息,谨慎上传
|
||||
label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db`
|
||||
description: |
|
||||
在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹
|
||||
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
|
||||
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
|
||||
**务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**
|
||||
render: shell
|
||||
|
||||
- type: checkboxes
|
||||
id: confirm-issue
|
||||
attributes:
|
||||
label: 我确认已在表单中附上了充足的补充说明以帮助开发人员确定问题
|
||||
description: 补充说明包括但不限于:日志文件、抛出的错误信息、截图和录屏
|
||||
options:
|
||||
- label: 是
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: confirm-no-duplicated-issue
|
||||
attributes:
|
||||
label: 我确认没有他人提出相同或类似的问题
|
||||
description: |
|
||||
请先通过 Issue 搜索功能确认这不是相同的问题;
|
||||
[BUG Issues](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aissue+is%3Aopen+label%3ABUG)
|
||||
你应该在原始 Issue 中通过回复添加有助于解决问题的信息,而不是创建重复的问题;
|
||||
**没有帮助的重复问题可能会被直接关闭**
|
||||
options:
|
||||
- label: 是
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: confirm-docs
|
||||
attributes:
|
||||
label: 我确认该问题没有在文档中解释
|
||||
description: Snap Hutao 官方文档:[https://hut.ao](https://hut.ao)
|
||||
options:
|
||||
- label: 是
|
||||
required: true
|
||||
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
|
||||

|
||||
|
||||
## 项目首页(文档)
|
||||
|
||||
[](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml)
|
||||
|
||||
[HUT.AO](https://hut.ao)
|
||||
|
||||
## 安装
|
||||
|
||||
* 前往 [下载页面](https://go.hut.ao/archive) 下载最新版本的 `胡桃` 安装包
|
||||
* 前往 [下载页面](https://go.hut.ao/down) 下载最新版本的 `胡桃` 安装包
|
||||
* (曾启用的可以跳过此步骤)在系统设置中打开 **开发者选项** 界面,勾选 `开发人员模式` 和 `允许 PowerShell 脚本`
|
||||
* 完全解压后,右键使用 powershell 运行 `install.ps1` 文件
|
||||
* 安装完成后可以关闭 `允许 PowerShell 脚本`
|
||||
|
||||
@@ -159,6 +159,7 @@ dotnet_diagnostic.CA1805.severity = suggestion
|
||||
|
||||
# VSTHRD111: Use ConfigureAwait(bool)
|
||||
dotnet_diagnostic.VSTHRD111.severity = suggestion
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Exception;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
@@ -48,6 +49,7 @@ public partial class App : Application
|
||||
Activation.Activate(firstInstance, activatedEventArgs);
|
||||
firstInstance.Activated += Activation.Activate;
|
||||
|
||||
logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version);
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
|
||||
|
||||
Ioc.Default
|
||||
|
||||
@@ -69,6 +69,8 @@ public class AppDbContext : DbContext
|
||||
/// <inheritdoc/>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new AvatarInfoConfiguration());
|
||||
modelBuilder
|
||||
.ApplyConfiguration(new AvatarInfoConfiguration())
|
||||
.ApplyConfiguration(new UserConfiguration());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Make ContentDialog's SmokeLayerBackground dsiplay over custom titleBar
|
||||
/// </summary>
|
||||
public class ContentDialogBehavior : BehaviorBase<ContentDialog>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ internal class I18NExtension : MarkupExtension
|
||||
static I18NExtension()
|
||||
{
|
||||
string currentName = CultureInfo.CurrentUICulture.Name;
|
||||
Type languageType = EnumerableExtensions.GetValueOrDefault(TranslationMap, currentName, typeof(LanguagezhCN));
|
||||
Translation = (ITranslation)Activator.CreateInstance(languageType)!;
|
||||
Type? languageType = TranslationMap.GetValueOrDefault2(currentName, typeof(LanguagezhCN));
|
||||
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
31
src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs
Normal file
31
src/Snap.Hutao/Snap.Hutao/Control/Markup/UriExtension.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
/// <summary>
|
||||
/// Uri扩展
|
||||
/// </summary>
|
||||
[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
|
||||
public sealed class UriExtension : MarkupExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的Uri扩展
|
||||
/// </summary>
|
||||
public UriExtension()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 地址
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
return new Uri(Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
29
src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml
Normal file
29
src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<UserControl
|
||||
x:Class="Snap.Hutao.Control.Panel.PanelSelector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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:shcm="using:Snap.Hutao.Control.Markup"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<SplitButton Padding="0,6" Click="SplitButtonClick" Loaded="SplitButtonLoaded">
|
||||
<SplitButton.Content>
|
||||
<FontIcon Name="IconPresenter" Glyph=""/>
|
||||
</SplitButton.Content>
|
||||
<SplitButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<RadioMenuFlyoutItem
|
||||
Tag="List"
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Text="列表"/>
|
||||
<RadioMenuFlyoutItem
|
||||
Tag="Grid"
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Text="网格"/>
|
||||
</MenuFlyout>
|
||||
</SplitButton.Flyout>
|
||||
</SplitButton>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
/// <summary>
|
||||
/// 面板选择器
|
||||
/// </summary>
|
||||
public sealed partial class PanelSelector : UserControl
|
||||
{
|
||||
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的面板选择器
|
||||
/// </summary>
|
||||
public PanelSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选择
|
||||
/// </summary>
|
||||
public string Current
|
||||
{
|
||||
get => (string)GetValue(CurrentProperty);
|
||||
set => SetValue(CurrentProperty, value);
|
||||
}
|
||||
|
||||
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
|
||||
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
|
||||
}
|
||||
|
||||
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
|
||||
int i = 0;
|
||||
for (; i < menuFlyout.Items.Count; i++)
|
||||
{
|
||||
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
|
||||
if (current.IsChecked)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
if (i > menuFlyout.Items.Count)
|
||||
{
|
||||
i = 1;
|
||||
}
|
||||
|
||||
if (i == menuFlyout.Items.Count)
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
|
||||
item.IsChecked = true;
|
||||
UpdateState(item);
|
||||
}
|
||||
|
||||
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
|
||||
UpdateState(item);
|
||||
}
|
||||
|
||||
private void UpdateState(RadioMenuFlyoutItem item)
|
||||
{
|
||||
Current = (string)item.Tag;
|
||||
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public class DescriptionTextBlock : ContentControl
|
||||
|
||||
if (i == description.Length - 1)
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendText(text, description[last..(i + 1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
@@ -139,7 +139,7 @@ public abstract class CacheBase<T>
|
||||
|
||||
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
|
||||
|
||||
if (item == null)
|
||||
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 0)
|
||||
{
|
||||
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
|
||||
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
|
||||
|
||||
@@ -17,12 +17,12 @@ internal static class CoreEnvironment
|
||||
/// <summary>
|
||||
/// 动态密钥1的盐
|
||||
/// </summary>
|
||||
public const string DynamicSecret1Salt = "Qqx8cyv7kuyD8fTw11SmvXSFHp7iZD29";
|
||||
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
|
||||
|
||||
/// <summary>
|
||||
/// 动态密钥2的盐
|
||||
/// </summary>
|
||||
public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk";
|
||||
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
@@ -32,7 +32,7 @@ internal static class CoreEnvironment
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabXrpcVersion = "2.37.1";
|
||||
public const string HoyolabXrpcVersion = "2.38.1";
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
|
||||
@@ -70,6 +70,83 @@ internal class DbCurrent<TEntity, TMessage>
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据库当前项
|
||||
/// 简化对数据库中选中项的管理
|
||||
/// </summary>
|
||||
/// <typeparam name="TObservable">绑定类型</typeparam>
|
||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal class DbCurrent<TObservable, TEntity, TMessage>
|
||||
where TObservable : class
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TObservable>
|
||||
{
|
||||
private readonly DbContext dbContext;
|
||||
private readonly DbSet<TEntity> dbSet;
|
||||
private readonly IMessenger messenger;
|
||||
private readonly Func<TObservable, TEntity> selector;
|
||||
|
||||
private TObservable? current;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="dbContext">数据库上下文</param>
|
||||
/// <param name="dbSet">数据集</param>
|
||||
/// <param name="selector">选择器</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.dbSet = dbSet;
|
||||
this.selector = selector;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
public TObservable? Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
// prevent useless sets
|
||||
if (current == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
{
|
||||
if (current != null)
|
||||
{
|
||||
TEntity entity = selector(current);
|
||||
entity.IsSelected = false;
|
||||
dbSet.Update(entity);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
|
||||
current = value;
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
TEntity entity = selector(current);
|
||||
entity.IsSelected = true;
|
||||
dbSet.Update(entity);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
|
||||
@@ -26,22 +26,20 @@ internal class ExceptionRecorder
|
||||
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当应用程序未经处理的异常引发时调用
|
||||
/// </summary>
|
||||
/// <param name="sender">实例</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
public void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常: [HResult:{code}]", e.Exception.HResult);
|
||||
// string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
// string fileName = $"ex-{DateTimeOffset.Now:yyyyMMddHHmmssffff}.txt";
|
||||
// File.WriteAllText(Path.Combine(path, fileName), $"{e.Exception}\r\n{e.Exception.StackTrace}");
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
|
||||
|
||||
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
|
||||
{
|
||||
provider.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xaml 绑定失败时触发
|
||||
/// </summary>
|
||||
/// <param name="sender">实例</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
public void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
|
||||
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
|
||||
{
|
||||
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
|
||||
}
|
||||
|
||||
@@ -25,4 +25,16 @@ internal static class Clipboard
|
||||
string json = await view.GetTextAsync();
|
||||
return JsonSerializer.Deserialize<T>(json, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置文本
|
||||
/// </summary>
|
||||
/// <param name="text">文本</param>
|
||||
public static void SetText(string text)
|
||||
{
|
||||
DataPackage content = new();
|
||||
content.SetText(text);
|
||||
Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content);
|
||||
Windows.ApplicationModel.DataTransfer.Clipboard.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
30
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniComment.cs
Normal file
30
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniComment.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 注释
|
||||
/// </summary>
|
||||
internal class IniComment : IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 Ini 注释
|
||||
/// </summary>
|
||||
/// <param name="comment">注释</param>
|
||||
public IniComment(string comment)
|
||||
{
|
||||
Comment = comment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注释
|
||||
/// </summary>
|
||||
public string Comment { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $";{Comment}";
|
||||
}
|
||||
}
|
||||
16
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniElement.cs
Normal file
16
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniElement.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 元素
|
||||
/// </summary>
|
||||
internal abstract class IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// 将当前元素转换到等价的字符串表示
|
||||
/// </summary>
|
||||
/// <returns>字符串</returns>
|
||||
public new abstract string ToString();
|
||||
}
|
||||
37
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniParameter.cs
Normal file
37
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniParameter.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 参数
|
||||
/// </summary>
|
||||
internal class IniParameter : IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Ini 参数
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
public IniParameter(string key, string value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 键
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Key}={Value}";
|
||||
}
|
||||
}
|
||||
31
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSection.cs
Normal file
31
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSection.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 节
|
||||
/// </summary>
|
||||
internal class IniSection : IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的Ini 节
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="elements">元素</param>
|
||||
public IniSection(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Name}]";
|
||||
}
|
||||
}
|
||||
47
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs
Normal file
47
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 序列化器
|
||||
/// </summary>
|
||||
internal static class IniSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步反序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">文件流</param>
|
||||
/// <returns>Ini 元素集合</returns>
|
||||
public static IEnumerable<IniElement> Deserialize(FileStream fileStream)
|
||||
{
|
||||
using (TextReader reader = new StreamReader(fileStream))
|
||||
{
|
||||
while (reader.ReadLine() is string line)
|
||||
{
|
||||
if (line.Length > 0)
|
||||
{
|
||||
if (line[0] == '[')
|
||||
{
|
||||
yield return new IniSection(line[1..^1]);
|
||||
}
|
||||
|
||||
if (line[0] == ';')
|
||||
{
|
||||
yield return new IniComment(line[1..]);
|
||||
}
|
||||
|
||||
if (line.IndexOf('=') > 0)
|
||||
{
|
||||
string[] parameters = line.Split('=', 2);
|
||||
yield return new IniParameter(parameters[0], parameters[1]);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -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<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
|
||||
|
||||
@@ -88,6 +88,16 @@ internal static class EventIds
|
||||
/// 成就
|
||||
/// </summary>
|
||||
public static readonly EventId Achievement = 100130;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计生成
|
||||
/// </summary>
|
||||
public static readonly EventId GachaStatisticGeneration = 100140;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计生成
|
||||
/// </summary>
|
||||
public static readonly EventId AvatarInfoGeneration = 100150;
|
||||
#endregion
|
||||
|
||||
#region 杂项
|
||||
|
||||
@@ -20,6 +20,8 @@ public sealed class LogEntryQueue : IDisposable
|
||||
private readonly TaskCompletionSource writeDbCompletionSource = new();
|
||||
private readonly LogDbContext logDbContext;
|
||||
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的日志队列
|
||||
/// </summary>
|
||||
@@ -27,7 +29,7 @@ public sealed class LogEntryQueue : IDisposable
|
||||
{
|
||||
logDbContext = InitializeDbContext();
|
||||
|
||||
Task.Run(async () => await WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
|
||||
Task.Run(() => WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,6 +45,11 @@ public sealed class LogEntryQueue : IDisposable
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// notify the write task to complete.
|
||||
disposeTokenSource.Cancel();
|
||||
|
||||
@@ -50,6 +57,7 @@ public sealed class LogEntryQueue : IDisposable
|
||||
writeDbCompletionSource.Task.GetAwaiter().GetResult();
|
||||
|
||||
logDbContext.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
private static LogDbContext InitializeDbContext()
|
||||
@@ -62,9 +70,8 @@ public sealed class LogEntryQueue : IDisposable
|
||||
logDbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
logDbContext.Logs.RemoveRange(logDbContext.Logs);
|
||||
logDbContext.SaveChanges();
|
||||
|
||||
// only raw sql can pass
|
||||
logDbContext.Database.ExecuteSqlRaw("DELETE FROM logs WHERE Exception IS NULL");
|
||||
return logDbContext;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
|
||||
|
||||
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 信号量扩展
|
||||
@@ -15,7 +15,7 @@ public static class SemaphoreSlimExtensions
|
||||
/// <returns>可释放的对象,用于释放信号量</returns>
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync();
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
return new SemaphoreSlimReleaser(semaphoreSlim);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Win32;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
using Windows.Win32.Foundation;
|
||||
@@ -149,14 +151,8 @@ internal sealed class ExtendedWindow
|
||||
{
|
||||
double scale = Persistence.GetScaleForWindow(handle);
|
||||
|
||||
List<RectInt32> dragRectsList = new();
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
RectInt32 dragRect = new((int)(48 * scale), 0, (int)(titleBar.ActualWidth * scale), (int)(titleBar.ActualHeight * scale));
|
||||
dragRectsList.Add(dragRect);
|
||||
|
||||
RectInt32[] dragRects = dragRectsList.ToArray();
|
||||
|
||||
appTitleBar.SetDragRectangles(dragRects);
|
||||
RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale);
|
||||
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// <see cref="BinaryReader"/> 扩展
|
||||
/// </summary>
|
||||
public static class BinaryReaderExtensions
|
||||
public static class BinaryReaderExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否处于流的结尾
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// <see cref="DateTimeOffset"/> 扩展
|
||||
/// </summary>
|
||||
public static class DateTimeOffsetExtensions
|
||||
public static class DateTimeOffsetExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 枚举拓展
|
||||
/// </summary>
|
||||
public static class EnumExtensions
|
||||
public static class EnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取枚举的描述
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
@@ -8,7 +9,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerable{T}"/> 扩展
|
||||
/// </summary>
|
||||
public static partial class EnumerableExtensions
|
||||
public static partial class EnumerableExtension
|
||||
{
|
||||
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
|
||||
public static double AverageNoThrow(this List<int> source)
|
||||
@@ -107,7 +108,7 @@ public static partial class EnumerableExtensions
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>结果值</returns>
|
||||
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
|
||||
public static TValue? GetValueOrDefault2<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
|
||||
where TKey : notnull
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out TValue? value))
|
||||
@@ -118,6 +119,53 @@ public static partial class EnumerableExtensions
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
|
||||
where TKey : notnull
|
||||
{
|
||||
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">增加的值</param>
|
||||
public static void Increase<TKey>(this Dictionary<TKey, int> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>是否存在键值</returns>
|
||||
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
|
||||
where TKey : notnull
|
||||
{
|
||||
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
|
||||
if (!Unsafe.IsNullRef(ref value))
|
||||
{
|
||||
++value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除表中首个满足条件的项
|
||||
/// </summary>
|
||||
@@ -153,6 +201,20 @@ public static partial class EnumerableExtensions
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
|
||||
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
Dictionary<TKey, TValue> dictionary = new();
|
||||
|
||||
foreach (TSource value in source)
|
||||
{
|
||||
dictionary[keySelector(value)] = valueSelector(value);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对 <see cref="TItem"/> 类型的计数器
|
||||
/// </summary>
|
||||
@@ -6,8 +6,19 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 数高性能扩展
|
||||
/// </summary>
|
||||
public static class NumberExtensions
|
||||
public static class NumberExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取从右向左某位上的数字
|
||||
/// </summary>
|
||||
/// <param name="x">源</param>
|
||||
/// <param name="place">位</param>
|
||||
/// <returns>数字</returns>
|
||||
public static int AtPlace(this int x, int place)
|
||||
{
|
||||
return (int)(x / Math.Pow(10, place - 1)) % 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算给定整数的位数
|
||||
/// </summary>
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
public static class ObjectExtensions
|
||||
public static class ObjectExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 包版本扩展
|
||||
/// </summary>
|
||||
public static class PackageVersionExtensions
|
||||
public static class PackageVersionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 将包版本转换为版本
|
||||
@@ -93,7 +93,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
|
||||
{
|
||||
Exception baseException = exception.GetBaseException();
|
||||
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(asyncRelayCommand));
|
||||
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace Snap.Hutao;
|
||||
/// 主窗体
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
[Injection(InjectAs.Singleton, typeof(Window))] // This is for IEnumerable<Window>
|
||||
[SuppressMessage("", "CA1001")]
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 词条评分
|
||||
/// </summary>
|
||||
public struct AffixScore
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的圣遗物评分
|
||||
/// </summary>
|
||||
/// <param name="score">评分</param>
|
||||
/// <param name="weight">最大值</param>
|
||||
public AffixScore(double score, double weight)
|
||||
{
|
||||
Score = score;
|
||||
Weight = weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public double Score { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 权重
|
||||
/// </summary>
|
||||
public double Weight { get; }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息
|
||||
/// </summary>
|
||||
public class Avatar
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public Uri Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 侧面图标
|
||||
/// </summary>
|
||||
public Uri SideIcon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 星级
|
||||
/// </summary>
|
||||
public ItemQuality Quality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 等级
|
||||
/// </summary>
|
||||
public string Level { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 好感度等级
|
||||
/// </summary>
|
||||
public int FetterLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public Weapon Weapon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物列表
|
||||
/// </summary>
|
||||
public List<Reliquary> Reliquaries { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 命之座列表
|
||||
/// </summary>
|
||||
public List<Constellation> Constellations { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 技能列表
|
||||
/// </summary>
|
||||
public List<Skill> Skills { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 属性
|
||||
/// </summary>
|
||||
public List<Pair2<string, string, string?>> Properties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public string Score { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 双爆评分
|
||||
/// </summary>
|
||||
public string CritScore { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 命座信息
|
||||
/// </summary>
|
||||
public class Constellation : NameIconDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否激活
|
||||
/// </summary>
|
||||
public bool IsActiviated { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 装备基类
|
||||
/// </summary>
|
||||
public class EquipBase : NameIconDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// 等级
|
||||
/// </summary>
|
||||
public string Level { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 品质
|
||||
/// </summary>
|
||||
public ItemQuality Quality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主属性
|
||||
/// </summary>
|
||||
public Pair<string, string> MainProperty { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 名称与描述抽象
|
||||
/// </summary>
|
||||
public abstract class NameIconDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public Uri Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家信息
|
||||
/// </summary>
|
||||
public class Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 昵称
|
||||
/// </summary>
|
||||
public string Nickname { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 等级
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签名
|
||||
/// </summary>
|
||||
public string Signature { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 完成成就数
|
||||
/// </summary>
|
||||
public int FinishAchievementNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 深渊层间
|
||||
/// </summary>
|
||||
public string SipralAbyssFloorLevel { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物
|
||||
/// </summary>
|
||||
public class Reliquary : EquipBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 副属性列表
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 初始词条
|
||||
/// </summary>
|
||||
public List<ReliquarySubProperty> PrimarySubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 强化词条
|
||||
/// </summary>
|
||||
public List<ReliquarySubProperty> SecondarySubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public double Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 格式化评分
|
||||
/// </summary>
|
||||
public string ScoreFormatted { get => $"{Score:F2}"; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物副词条
|
||||
/// </summary>
|
||||
public class ReliquarySubProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造副属性
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <param name="score">评分</param>
|
||||
public ReliquarySubProperty(string name, string value, double score)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
Score = score;
|
||||
|
||||
// only 0.2 | 0.4 | 0.6 | 0.8 | 1.0
|
||||
Opacity = score switch
|
||||
{
|
||||
< 25 => 0.25,
|
||||
< 50 => 0.5,
|
||||
< 75 => 0.75,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public double Score { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度
|
||||
/// </summary>
|
||||
public double Opacity { get; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 天赋
|
||||
/// </summary>
|
||||
public class Skill : NameIconDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// 技能属性
|
||||
/// </summary>
|
||||
public LevelParam<string, ParameterInfo> Info { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家与角色列表的包装器
|
||||
/// </summary>
|
||||
public class Summary
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家信息
|
||||
/// </summary>
|
||||
public Player Player { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 角色列表
|
||||
/// </summary>
|
||||
public List<Avatar> Avatars { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public class Weapon : EquipBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 副属性
|
||||
/// </summary>
|
||||
public Pair<string, string> SubProperty { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 精炼属性
|
||||
/// </summary>
|
||||
public string AffixLevel { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 精炼名称
|
||||
/// </summary>
|
||||
public string AffixName { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 精炼被动
|
||||
/// </summary>
|
||||
public string AffixDescription { get; set; } = default!;
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using EntityUser = Snap.Hutao.Model.Entity.User;
|
||||
|
||||
@@ -56,7 +55,7 @@ public class User : Observable
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityUser.Cookie"/>
|
||||
public string? Cookie
|
||||
public Cookie Cookie
|
||||
{
|
||||
get => inner.Cookie;
|
||||
set => inner.Cookie = value;
|
||||
@@ -72,27 +71,6 @@ public class User : Observable
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; }
|
||||
|
||||
/// <summary>
|
||||
/// 将cookie的字符串形式转换为字典
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie的字符串形式</param>
|
||||
/// <returns>包含cookie信息的字典</returns>
|
||||
public static IDictionary<string, string> MapCookie(string cookie)
|
||||
{
|
||||
SortedDictionary<string, string> cookieDictionary = new();
|
||||
|
||||
string[] values = cookie.TrimEnd(';').Split(';');
|
||||
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
|
||||
{
|
||||
string cookieName = parts[0].Trim();
|
||||
string cookieValue = parts.Length == 1 ? string.Empty : parts[1].Trim();
|
||||
|
||||
cookieDictionary.Add(cookieName, cookieValue);
|
||||
}
|
||||
|
||||
return cookieDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库恢复用户
|
||||
/// </summary>
|
||||
@@ -108,114 +86,30 @@ public class User : Observable
|
||||
CancellationToken token = default)
|
||||
{
|
||||
User user = new(inner);
|
||||
bool successful = await user.ResumeInternalAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
return successful ? user : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化用户
|
||||
/// 创建并初始化用户
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie</param>
|
||||
/// <param name="userClient">用户客户端</param>
|
||||
/// <param name="userGameRoleClient">角色客户端</param>
|
||||
/// <param name="authClient">授权客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="false"/> </returns>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="null"/> </returns>
|
||||
internal static async Task<User?> CreateAsync(
|
||||
IDictionary<string, string> cookie,
|
||||
Cookie cookie,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
AuthClient authClient,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
string simplifiedCookie = ToCookieString(cookie);
|
||||
EntityUser inner = EntityUser.Create(simplifiedCookie);
|
||||
User user = new(inner);
|
||||
bool successful = await user.CreateInternalAsync(cookie, userClient, userGameRoleClient, authClient, token).ConfigureAwait(false);
|
||||
User user = new(EntityUser.Create(cookie));
|
||||
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
return successful ? user : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试升级到Stoken
|
||||
/// </summary>
|
||||
/// <param name="addition">额外的token</param>
|
||||
/// <param name="authClient">验证客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否升级成功</returns>
|
||||
internal async Task<bool> TryUpgradeByLoginTicketAsync(IDictionary<string, string> addition, AuthClient authClient, CancellationToken token)
|
||||
{
|
||||
IDictionary<string, string> cookie = MapCookie(Cookie!);
|
||||
if (addition.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
|
||||
{
|
||||
cookie[CookieKeys.LOGIN_TICKET] = loginTicket;
|
||||
}
|
||||
|
||||
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? loginUid))
|
||||
{
|
||||
cookie[CookieKeys.LOGIN_UID] = loginUid;
|
||||
}
|
||||
|
||||
bool result = await TryRequestStokenAndAddToCookieAsync(cookie, authClient, token).ConfigureAwait(false);
|
||||
|
||||
if (result)
|
||||
{
|
||||
Cookie = ToCookieString(cookie);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 Stoken
|
||||
/// </summary>
|
||||
/// <param name="addition">额外的cookie</param>
|
||||
internal void AddStoken(IDictionary<string, string> addition)
|
||||
{
|
||||
IDictionary<string, string> cookie = MapCookie(Cookie!);
|
||||
|
||||
if (addition.TryGetValue(CookieKeys.STOKEN, out string? stoken))
|
||||
{
|
||||
cookie[CookieKeys.STOKEN] = stoken;
|
||||
}
|
||||
|
||||
if (addition.TryGetValue(CookieKeys.STUID, out string? stuid))
|
||||
{
|
||||
cookie[CookieKeys.STUID] = stuid;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToCookieString(IDictionary<string, string> cookie)
|
||||
{
|
||||
return string.Join(';', cookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
}
|
||||
|
||||
private static async Task<bool> TryRequestStokenAndAddToCookieAsync(IDictionary<string, string> cookie, AuthClient authClient, CancellationToken token)
|
||||
{
|
||||
if (cookie.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
|
||||
{
|
||||
string? loginUid = cookie.GetValueOrDefault(CookieKeys.LOGIN_UID) ?? cookie.GetValueOrDefault(CookieKeys.LTUID);
|
||||
|
||||
if (loginUid != null)
|
||||
{
|
||||
Dictionary<string, string> stokens = await authClient
|
||||
.GetMultiTokenByLoginTicketAsync(loginTicket, loginUid, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (stokens.TryGetValue(CookieKeys.STOKEN, out string? stoken) && stokens.TryGetValue(CookieKeys.LTOKEN, out string? ltoken))
|
||||
{
|
||||
cookie[CookieKeys.STOKEN] = stoken;
|
||||
cookie[CookieKeys.LTOKEN] = ltoken;
|
||||
cookie[CookieKeys.STUID] = cookie[CookieKeys.LTUID];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> ResumeInternalAsync(
|
||||
private async Task<bool> InitializeCoreAsync(
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
CancellationToken token = default)
|
||||
@@ -225,38 +119,14 @@ public class User : Observable
|
||||
return true;
|
||||
}
|
||||
|
||||
await PrepareUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
await InitializeUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
|
||||
private async Task<bool> CreateInternalAsync(
|
||||
IDictionary<string, string> cookie,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
AuthClient authClient,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await TryRequestStokenAndAddToCookieAsync(cookie, authClient, token).ConfigureAwait(false))
|
||||
{
|
||||
Cookie = ToCookieString(cookie);
|
||||
}
|
||||
|
||||
await PrepareUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
|
||||
private async Task PrepareUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
|
||||
private async Task InitializeUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
|
||||
{
|
||||
UserInfo = await userClient
|
||||
.GetUserFullInfoAsync(this, token)
|
||||
|
||||
@@ -28,4 +28,15 @@ public class AvatarInfo
|
||||
/// 角色的信息
|
||||
/// </summary>
|
||||
public Web.Enka.Model.AvatarInfo Info { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的实体角色信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="info">角色信息</param>
|
||||
/// <returns>实体角色信息</returns>
|
||||
public static AvatarInfo Create(string uid, Web.Enka.Model.AvatarInfo info)
|
||||
{
|
||||
return new() { Uid = uid, Info = info };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 用户配置
|
||||
/// </summary>
|
||||
internal class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.Property(e => e.Cookie)
|
||||
.HasColumnType("TEXT")
|
||||
.HasConversion(
|
||||
e => e == null ? string.Empty : e.ToString(),
|
||||
e => Cookie.Parse(e));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -28,14 +29,14 @@ public class User : ISelectable
|
||||
/// <summary>
|
||||
/// 用户的Cookie
|
||||
/// </summary>
|
||||
public string? Cookie { get; set; }
|
||||
public Cookie Cookie { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的用户
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie</param>
|
||||
/// <returns>新创建的用户</returns>
|
||||
public static User Create(string cookie)
|
||||
public static User Create(Cookie cookie)
|
||||
{
|
||||
return new() { Cookie = cookie };
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class UIAFInfo
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
// Hot fix | 1.0.31 | UIAF.Info.ExportTimestamp can be milliseconds
|
||||
get => DateTimeOffsetExtensions.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,9 +32,10 @@ public class UIGFInfo
|
||||
/// <summary>
|
||||
/// 导出时间
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
get => DateTimeOffsetExtensions.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,10 +5,15 @@ namespace Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
/// <summary>
|
||||
/// 成就信息状态
|
||||
/// https://github.com/Grasscutters/Grasscutter/blob/development/proto/AchievementInfo.proto
|
||||
/// https://github.com/Grasscutters/Grasscutter/blob/development/src/generated/main/java/emu/grasscutter/net/proto/AchievementInfoOuterClass.java#L163
|
||||
/// </summary>
|
||||
public enum AchievementInfoStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 未识别
|
||||
/// </summary>
|
||||
UNRECOGNIZED = -1,
|
||||
|
||||
/// <summary>
|
||||
/// 非法值
|
||||
/// </summary>
|
||||
|
||||
@@ -24,8 +24,10 @@ public enum FightProperty
|
||||
FIGHT_PROP_BASE_HP = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 生命值加成
|
||||
/// 小生命值加成
|
||||
/// </summary>
|
||||
[Description("生命值")]
|
||||
[Format(FormatMethod.Integer)]
|
||||
FIGHT_PROP_HP = 2,
|
||||
|
||||
/// <summary>
|
||||
@@ -45,6 +47,8 @@ public enum FightProperty
|
||||
/// <summary>
|
||||
/// 攻击力加成
|
||||
/// </summary>
|
||||
[Description("攻击力")]
|
||||
[Format(FormatMethod.Integer)]
|
||||
FIGHT_PROP_ATTACK = 5,
|
||||
|
||||
/// <summary>
|
||||
@@ -64,6 +68,8 @@ public enum FightProperty
|
||||
/// <summary>
|
||||
/// 防御力加成
|
||||
/// </summary>
|
||||
[Description("防御力")]
|
||||
[Format(FormatMethod.Integer)]
|
||||
FIGHT_PROP_DEFENSE = 8,
|
||||
|
||||
/// <summary>
|
||||
@@ -387,6 +393,8 @@ public enum FightProperty
|
||||
/// <summary>
|
||||
/// 最大生命值
|
||||
/// </summary>
|
||||
[Description("生命值")]
|
||||
[Format(FormatMethod.Integer)]
|
||||
FIGHT_PROP_MAX_HP = 2000,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,7 +15,7 @@ public enum ItemType
|
||||
ITEM_NONE = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 贵重道具
|
||||
/// 虚拟道具
|
||||
/// </summary>
|
||||
ITEM_VIRTUAL = 1,
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ public enum WeaponType
|
||||
WEAPON_SWORD_ONE_HAND = 1,
|
||||
|
||||
#region Not Used
|
||||
|
||||
/// <summary>
|
||||
/// ?
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 指示该类为简述统计物品的源
|
||||
/// </summary>
|
||||
public interface ISummaryItemSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 星级
|
||||
/// </summary>
|
||||
ItemQuality Quality { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 转换到简述统计物品
|
||||
/// </summary>
|
||||
/// <param name="lastPull">距上个五星</param>
|
||||
/// <param name="time">时间</param>
|
||||
/// <param name="isUp">是否为Up物品</param>
|
||||
/// <returns>简述统计物品</returns>
|
||||
SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
public class Avatar : IStatisticsItemSource, INameQuality
|
||||
public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
@@ -136,4 +136,4 @@ public class Avatar : IStatisticsItemSource, INameQuality
|
||||
IsUp = isUp,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
Normal file
74
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public static class AvatarIds
|
||||
{
|
||||
public const int Ayaka = 10000002;
|
||||
public const int Qin = 10000003;
|
||||
|
||||
public const int Lisa = 10000006;
|
||||
|
||||
public const int Barbara = 10000014;
|
||||
public const int Kaeya = 10000015;
|
||||
public const int Diluc = 10000016;
|
||||
|
||||
public const int Razor = 10000020;
|
||||
public const int Ambor = 10000021;
|
||||
public const int Venti = 10000022;
|
||||
public const int Xiangling = 10000023;
|
||||
public const int Beidou = 10000024;
|
||||
public const int Xingqiu = 10000025;
|
||||
public const int Xiao = 10000026;
|
||||
public const int Ningguang = 10000027;
|
||||
|
||||
public const int Klee = 10000029;
|
||||
public const int Zhongli = 10000030;
|
||||
public const int Fischl = 10000031;
|
||||
public const int Bennett = 10000032;
|
||||
public const int Tartaglia = 10000033;
|
||||
public const int Noel = 10000034;
|
||||
public const int Qiqi = 10000035;
|
||||
public const int Chongyun = 10000036;
|
||||
public const int Ganyu = 10000037;
|
||||
public const int Albedo = 10000038;
|
||||
public const int Diona = 10000039;
|
||||
|
||||
public const int Mona = 10000041;
|
||||
public const int Keqing = 10000042;
|
||||
public const int Sucrose = 10000043;
|
||||
public const int Xinyan = 10000044;
|
||||
public const int Rosaria = 10000045;
|
||||
public const int Hutao = 10000046;
|
||||
public const int Kazuha = 10000047;
|
||||
public const int Feiyan = 10000048;
|
||||
public const int Yoimiya = 10000049;
|
||||
public const int Tohma = 10000050;
|
||||
public const int Eula = 10000051;
|
||||
public const int Shougun = 10000052;
|
||||
public const int Sayu = 10000053;
|
||||
public const int Kokomi = 10000054;
|
||||
public const int Gorou = 10000055;
|
||||
public const int Sara = 10000056;
|
||||
public const int Itto = 10000057;
|
||||
public const int Yae = 10000058;
|
||||
public const int Heizou = 10000059;
|
||||
public const int Yelan = 10000060;
|
||||
|
||||
public const int Aloy = 10000062;
|
||||
public const int Shenhe = 10000063;
|
||||
public const int Yunjin = 10000064;
|
||||
public const int Shinobu = 10000065;
|
||||
public const int Ayato = 10000066;
|
||||
public const int Collei = 10000067;
|
||||
public const int Dori = 10000068;
|
||||
public const int Tighnari = 10000069;
|
||||
public const int Nilou = 10000070;
|
||||
public const int Cyno = 10000071;
|
||||
public const int Candace = 10000072;
|
||||
}
|
||||
@@ -27,4 +27,14 @@ public class Costume
|
||||
/// 是否为默认
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public string? Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 侧面图标
|
||||
/// </summary>
|
||||
public string? SideIcon { get; set; }
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class SkillDepot
|
||||
public IList<ProudableSkill> Inherents { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 全部天赋
|
||||
/// 全部天赋,包括固有天赋
|
||||
/// </summary>
|
||||
public IList<ProudableSkill> CompositeSkills
|
||||
{
|
||||
@@ -36,6 +36,24 @@ public class SkillDepot
|
||||
/// </summary>
|
||||
public IList<SkillBase> Talents { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 获取无固有天赋的技能列表
|
||||
/// </summary>
|
||||
/// <returns>天赋列表</returns>
|
||||
public IEnumerable<ProudableSkill> GetCompositeSkillsNoInherents()
|
||||
{
|
||||
foreach (ProudableSkill skill in Skills)
|
||||
{
|
||||
// skip skills like Mona's & Ayaka's shift
|
||||
if (skill.Proud.Parameters.Count > 1)
|
||||
{
|
||||
yield return skill;
|
||||
}
|
||||
}
|
||||
|
||||
yield return EnergySkill;
|
||||
}
|
||||
|
||||
private IEnumerable<ProudableSkill> GetCompositeSkills()
|
||||
{
|
||||
foreach (ProudableSkill skill in Skills)
|
||||
|
||||
@@ -12,22 +12,43 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// </summary>
|
||||
internal sealed class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取特定等级的解释
|
||||
/// </summary>
|
||||
/// <param name="from">源</param>
|
||||
/// <param name="level">等级</param>
|
||||
/// <returns>特定等级的解释</returns>
|
||||
public static LevelParam<string, ParameterInfo> Convert(DescParam from, int level)
|
||||
{
|
||||
// DO NOT INLINE!
|
||||
// Cache the formats
|
||||
IList<DescFormat> formats = from.Descriptions
|
||||
.Select(desc => new DescFormat(desc))
|
||||
.ToList();
|
||||
|
||||
LevelParam<int, double> param = from.Parameters.Single(param => param.Level == level);
|
||||
|
||||
return new LevelParam<string, ParameterInfo>($"Lv.{param.Level}", GetParameterInfos(formats, param.Parameters));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IList<LevelParam<string, ParameterInfo>> Convert(DescParam from)
|
||||
{
|
||||
// DO NOT INLINE!
|
||||
// Cache the formats
|
||||
IList<DescFormat> formats = from.Descriptions
|
||||
.Select(desc => new DescFormat(desc))
|
||||
.ToList();
|
||||
|
||||
IList<LevelParam<string, ParameterInfo>> parameters = from.Parameters
|
||||
.Select(param => new LevelParam<string, ParameterInfo>(param.Level.ToString(), GetParameterInfos(from, param.Parameters)))
|
||||
.Select(param => new LevelParam<string, ParameterInfo>(param.Level.ToString(), GetParameterInfos(formats, param.Parameters)))
|
||||
.ToList();
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static IList<ParameterInfo> GetParameterInfos(DescParam rawDescParam, IList<double> param)
|
||||
private static IList<ParameterInfo> GetParameterInfos(IList<DescFormat> formats, IList<double> param)
|
||||
{
|
||||
IList<DescFormat> formats = rawDescParam.Descriptions
|
||||
.Select(desc => new DescFormat(desc))
|
||||
.ToList();
|
||||
|
||||
List<ParameterInfo> results = new();
|
||||
|
||||
for (int index = 0; index < formats.Count; index++)
|
||||
|
||||
@@ -27,4 +27,4 @@ internal class EquipIconConverter : ValueConverterBase<string, Uri>
|
||||
{
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,70 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// </summary>
|
||||
internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 格式化对
|
||||
/// </summary>
|
||||
/// <param name="property">属性</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>对</returns>
|
||||
public static Pair<string, string> FormatPair(FightProperty property, double value)
|
||||
{
|
||||
return new(property.GetDescription(), FormatValue(property, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化 对2
|
||||
/// </summary>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="method">方法</param>
|
||||
/// <param name="value1">值1</param>
|
||||
/// <param name="value2">值2</param>
|
||||
/// <returns>对2</returns>
|
||||
public static Pair2<string, string, string?> FormatIntegerPair2(string name, FormatMethod method, double value1, double value2)
|
||||
{
|
||||
return new(name, FormatValue(method, value1), $"[+{FormatValue(method, value2)}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化 对2
|
||||
/// </summary>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="method">方法</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>对2</returns>
|
||||
public static Pair2<string, string, string?> FormatIntegerPair2(string name, FormatMethod method, double value)
|
||||
{
|
||||
return new(name, FormatValue(method, value), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化战斗属性
|
||||
/// </summary>
|
||||
/// <param name="property">战斗属性</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>格式化的值</returns>
|
||||
public static string FormatValue(FightProperty property, double value)
|
||||
{
|
||||
return FormatValue(property.GetFormatMethod(), value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化战斗属性
|
||||
/// </summary>
|
||||
/// <param name="method">格式化方法</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>格式化的值</returns>
|
||||
public static string FormatValue(FormatMethod method, double value)
|
||||
{
|
||||
string valueFormatted = method switch
|
||||
{
|
||||
FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(),
|
||||
FormatMethod.Percent => string.Format("{0:P1}", value),
|
||||
_ => value.ToString(),
|
||||
};
|
||||
return valueFormatted;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IList<LevelParam<string, ParameterInfo>> Convert(PropertyInfo from)
|
||||
{
|
||||
@@ -34,14 +98,7 @@ internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<L
|
||||
for (int index = 0; index < parameters.Count; index++)
|
||||
{
|
||||
double param = parameters[index];
|
||||
FormatMethod method = properties[index].GetFormatMethod();
|
||||
|
||||
string valueFormatted = method switch
|
||||
{
|
||||
FormatMethod.Integer => Math.Round((double)param, MidpointRounding.AwayFromZero).ToString(),
|
||||
FormatMethod.Percent => string.Format("{0:P1}", param),
|
||||
_ => param.ToString(),
|
||||
};
|
||||
string valueFormatted = FormatValue(properties[index], param);
|
||||
|
||||
results.Add(new ParameterInfo { Description = properties[index].GetDescription(), Parameter = valueFormatted });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 品质颜色转换器
|
||||
/// </summary>
|
||||
internal class QualityColorConverter : ValueConverterBase<ItemQuality, Color>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 武器图片转换器
|
||||
/// </summary>
|
||||
internal class RelicIconConverter : ValueConverterBase<string, Uri>
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/RelicIcon/{0}.png";
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri IconNameToUri(string name)
|
||||
{
|
||||
return new Uri(string.Format(BaseUrl, name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(string from)
|
||||
{
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
@@ -13,16 +13,26 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
||||
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
|
||||
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(string from)
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(string from)
|
||||
{
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class Reliquary
|
||||
/// <summary>
|
||||
/// 允许出现的等级
|
||||
/// </summary>
|
||||
public IEnumerable<ItemQuality> RankLevels { get; set; } = default!;
|
||||
public ItemQuality RankLevel { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 套装Id
|
||||
|
||||
@@ -12,4 +12,4 @@ public class ReliquaryAffix : ReliquaryAffixBase
|
||||
/// 值
|
||||
/// </summary>
|
||||
public double Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,4 @@ public class ReliquaryAffixBase
|
||||
/// 战斗属性
|
||||
/// </summary>
|
||||
public FightProperty Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物等级
|
||||
/// </summary>
|
||||
public class ReliquaryLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// 品质
|
||||
/// </summary>
|
||||
public ItemQuality Quality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 等级 1-21
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumKeyDictionaryConverter))]
|
||||
public IDictionary<FightProperty, double> Properties { get; set; } = default!;
|
||||
}
|
||||
21
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs
Normal file
21
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/AffixInfo.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Weapon;
|
||||
|
||||
/// <summary>
|
||||
/// 武器被动信息
|
||||
/// </summary>
|
||||
public class AffixInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 被动的名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 各个等级的描述
|
||||
/// 0-4
|
||||
/// </summary>
|
||||
public List<LevelDescription<int>> Descriptions { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Weapon;
|
||||
|
||||
/// <summary>
|
||||
/// 等级与描述
|
||||
/// </summary>
|
||||
/// <typeparam name="TLevel">等级的类型</typeparam>
|
||||
public class LevelDescription<TLevel>
|
||||
{
|
||||
/// <summary>
|
||||
/// 等级
|
||||
/// </summary>
|
||||
public TLevel Level { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = default!;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Weapon;
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public class Weapon : IStatisticsItemSource, INameQuality
|
||||
public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
@@ -54,9 +54,17 @@ public class Weapon : IStatisticsItemSource, INameQuality
|
||||
/// </summary>
|
||||
public PropertyInfo Property { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 被动信息, 无被动的武器为 <see langword="null"/>
|
||||
/// </summary>
|
||||
public AffixInfo? Affix { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public ItemQuality Quality => RankLevel;
|
||||
public ItemQuality Quality
|
||||
{
|
||||
get => RankLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为基础物品
|
||||
@@ -110,4 +118,4 @@ public class Weapon : IStatisticsItemSource, INameQuality
|
||||
IsUp = isUp,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs
Normal file
41
src/Snap.Hutao/Snap.Hutao/Model/Pair2.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 对
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键的类型</typeparam>
|
||||
/// <typeparam name="TValue1">值1的类型</typeparam>
|
||||
/// <typeparam name="TValue2">值2的类型</typeparam>
|
||||
public class Pair2<TKey, TValue1, TValue2>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的对
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value1">值1</param>
|
||||
/// <param name="value2">值2</param>
|
||||
public Pair2(TKey key, TValue1 value1, TValue2 value2)
|
||||
{
|
||||
Key = key;
|
||||
Value1 = value1;
|
||||
Value2 = value2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 键
|
||||
/// </summary>
|
||||
public TKey Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public TValue1 Value1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public TValue2 Value2 { get; set; }
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.1.4.0" />
|
||||
Version="1.1.11.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Snap.Hutao;
|
||||
/// <summary>
|
||||
/// Program class
|
||||
/// </summary>
|
||||
public static class Program
|
||||
public static partial class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 主线程队列
|
||||
@@ -62,8 +62,8 @@ public static class Program
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder
|
||||
.AddDatabase()
|
||||
.AddDebug())
|
||||
.AddDebug()
|
||||
.AddDatabase())
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
|
||||
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_BoostUp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Locked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 服务
|
||||
/// </summary>
|
||||
internal interface IHutaoService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Enka;
|
||||
using Snap.Hutao.Web.Enka.Model;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAvatarInfoService))]
|
||||
internal class AvatarInfoService : IAvatarInfoService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly ISummaryFactory summaryFactory;
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ILogger<AvatarInfoService> logger;
|
||||
private readonly EnkaClient enkaClient;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色信息服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="summaryFactory">简述工厂</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="enkaClient">Enka客户端</param>
|
||||
public AvatarInfoService(
|
||||
AppDbContext appDbContext,
|
||||
IMetadataService metadataService,
|
||||
ISummaryFactory summaryFactory,
|
||||
ILogger<AvatarInfoService> logger,
|
||||
EnkaClient enkaClient)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.metadataService = metadataService;
|
||||
this.summaryFactory = summaryFactory;
|
||||
this.logger = logger;
|
||||
this.enkaClient = enkaClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default)
|
||||
{
|
||||
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
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<Web.Enka.Model.AvatarInfo> list = HasOption(refreshOption, RefreshOption.StoreInDatabase)
|
||||
? UpdateDbAvatarInfo(uid.Value, resp.AvatarInfoList)
|
||||
: resp.AvatarInfoList;
|
||||
|
||||
Summary summary = await GetSummaryCoreAsync(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 GetSummaryCoreAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(RefreshResult.MetadataUninitialized, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasOption(RefreshOption source, RefreshOption define)
|
||||
{
|
||||
return (source & define) == define;
|
||||
}
|
||||
|
||||
private async Task<Summary> GetSummaryCoreAsync(PlayerInfo info, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos)
|
||||
{
|
||||
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
|
||||
Summary summary = await summaryFactory.CreateAsync(info, avatarInfos).ConfigureAwait(false);
|
||||
logger.LogInformation(EventIds.AvatarInfoGeneration, "AvatarInfoSummary Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
private async Task<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
|
||||
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private List<Web.Enka.Model.AvatarInfo> UpdateDbAvatarInfo(string uid, IEnumerable<Web.Enka.Model.AvatarInfo> webInfos)
|
||||
{
|
||||
List<Model.Entity.AvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
foreach (Web.Enka.Model.AvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (webInfo.AvatarId == 10000005 || webInfo.AvatarId == 10000007)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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<Web.Enka.Model.AvatarInfo> GetDbAvatarInfos(string uid)
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
|
||||
// .AsEnumerable()
|
||||
// .OrderByDescending(i => i.AvatarId)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 词条权重
|
||||
/// </summary>
|
||||
internal class AffixWeight : Dictionary<FightProperty, double>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的词条权重
|
||||
/// </summary>
|
||||
/// <param name="avatarId">角色Id</param>
|
||||
/// <param name="hp">大生命</param>
|
||||
/// <param name="atk">大攻击</param>
|
||||
/// <param name="def">大防御</param>
|
||||
/// <param name="cr">暴击率</param>
|
||||
/// <param name="ch">暴击伤害</param>
|
||||
/// <param name="em">元素精通</param>
|
||||
/// <param name="ce">充能效率</param>
|
||||
/// <param name="ha">治疗加成</param>
|
||||
/// <param name="name">名称</param>
|
||||
public AffixWeight(int avatarId, double hp, double atk, double def, double cr, double ch, double em, double ce, double ha, string name = "通用")
|
||||
{
|
||||
AvatarId = avatarId;
|
||||
Name = name;
|
||||
|
||||
this[FightProperty.FIGHT_PROP_HP_PERCENT] = hp;
|
||||
this[FightProperty.FIGHT_PROP_ATTACK_PERCENT] = atk;
|
||||
this[FightProperty.FIGHT_PROP_DEFENSE_PERCENT] = def;
|
||||
this[FightProperty.FIGHT_PROP_CRITICAL] = cr;
|
||||
this[FightProperty.FIGHT_PROP_CRITICAL_HURT] = ch;
|
||||
this[FightProperty.FIGHT_PROP_ELEMENT_MASTERY] = em;
|
||||
this[FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY] = ce;
|
||||
this[FightProperty.FIGHT_PROP_HEAL_ADD] = ha;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色Id
|
||||
/// </summary>
|
||||
public int AvatarId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 简述工厂
|
||||
/// </summary>
|
||||
internal interface ISummaryFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步创建简述对象
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">玩家信息</param>
|
||||
/// <param name="avatarInfos">角色列表</param>
|
||||
/// <returns>简述对象</returns>
|
||||
Task<Summary> CreateAsync(PlayerInfo playerInfo, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 权重配置
|
||||
/// </summary>
|
||||
internal static partial class ReliquaryWeightConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认
|
||||
/// </summary>
|
||||
public static readonly AffixWeight Default = new(0, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } };
|
||||
|
||||
/// <summary>
|
||||
/// 词条权重
|
||||
/// https://docs.qq.com/sheet/DUG52SFJlTUN3cmNL?tab=BB08J2
|
||||
/// </summary>
|
||||
public static readonly List<AffixWeight> AffixWeights = new()
|
||||
{
|
||||
new(AvatarIds.Ayaka, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Qin, 0, 75, 0, 100, 100, 0, 55, 100) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Lisa, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Barbara, 100, 50, 0, 50, 50, 0, 55, 100) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 80 } },
|
||||
new(AvatarIds.Barbara, 50, 75, 0, 100, 100, 0, 55, 100, "暴力奶妈") { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Kaeya, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Diluc, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Razor, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 50 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Ambor, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Venti, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Xiangling, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Beidou, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Xingqiu, 0, 75, 0, 100, 100, 0, 75, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Xiao, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Ningguang, 0, 75, 0, 100, 100, 0, 30, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Klee, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Zhongli, 80, 75, 0, 100, 100, 0, 55, 0, "武神钟离") { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 50 } },
|
||||
new(AvatarIds.Zhongli, 100, 55, 0, 100, 100, 0, 55, 0, "血牛钟离") { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 75 } },
|
||||
new(AvatarIds.Zhongli, 100, 55, 0, 100, 100, 0, 75, 0, "血牛钟离(2命+)") { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 75 } },
|
||||
new(AvatarIds.Fischl, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Bennett, 100, 50, 0, 100, 100, 0, 55, 100) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 70 } },
|
||||
new(AvatarIds.Tartaglia, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Noel, 0, 50, 90, 100, 100, 0, 70, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Qiqi, 0, 100, 0, 100, 100, 0, 55, 100) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 60 } },
|
||||
new(AvatarIds.Chongyun, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Ganyu, 0, 75, 0, 100, 100, 75, 0, 0, "融化流") { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Ganyu, 0, 75, 0, 100, 100, 0, 55, 0, "永冻流") { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Albedo, 0, 0, 100, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Diona, 100, 50, 0, 50, 50, 0, 90, 100) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Mona, 0, 75, 0, 100, 100, 75, 75, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Keqing, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Sucrose, 0, 75, 0, 100, 100, 100, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 40 } },
|
||||
new(AvatarIds.Xinyan, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 50 } },
|
||||
new(AvatarIds.Rosaria, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 70 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 80 } },
|
||||
new(AvatarIds.Hutao, 80, 50, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Kazuha, 0, 75, 0, 100, 100, 100, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Feiyan, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Yoimiya, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Tohma, 100, 50, 0, 50, 50, 0, 90, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 75 } },
|
||||
new(AvatarIds.Eula, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 40 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Shougun, 0, 75, 0, 100, 100, 0, 90, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 75 } },
|
||||
new(AvatarIds.Sayu, 0, 50, 0, 50, 50, 100, 55, 100) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 80 } },
|
||||
new(AvatarIds.Kokomi, 100, 50, 0, 0, 0, 0, 55, 100) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Gorou, 0, 50, 100, 50, 50, 0, 90, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 25 } },
|
||||
new(AvatarIds.Sara, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Itto, 0, 50, 100, 100, 100, 0, 30, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Yae, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Heizou, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Yelan, 80, 0, 0, 100, 100, 0, 75, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Aloy, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Shenhe, 0, 100, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Yunjin, 0, 0, 100, 50, 50, 0, 90, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 25 } },
|
||||
new(AvatarIds.Shinobu, 100, 50, 0, 100, 100, 75, 55, 100) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Ayato, 50, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Collei, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_GRASS_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Dori, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Tighnari, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_GRASS_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Nilou, 100, 75, 0, 100, 100, 0, 55, 0, "直伤流") { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Nilou, 100, 75, 0, 100, 100, 0, 55, 0, "反应流") { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Cyno, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Candace, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.Model.Metadata.Reliquary;
|
||||
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 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;
|
||||
|
||||
/// <summary>
|
||||
/// 简述角色工厂
|
||||
/// </summary>
|
||||
internal class SummaryAvatarFactory
|
||||
{
|
||||
private readonly Dictionary<int, MetadataAvatar> idAvatarMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
private readonly ModelAvatarInfo avatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色工厂
|
||||
/// </summary>
|
||||
/// <param name="idAvatarMap">角色映射</param>
|
||||
/// <param name="idWeaponMap">武器映射</param>
|
||||
/// <param name="idRelicMainPropMap">圣遗物主属性映射</param>
|
||||
/// <param name="idReliquaryAffixMap">圣遗物副词条映射</param>
|
||||
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
|
||||
/// <param name="reliquaries">圣遗物</param>
|
||||
/// <param name="avatarInfo">角色信息</param>
|
||||
public SummaryAvatarFactory(
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap,
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> reliquaries,
|
||||
ModelAvatarInfo avatarInfo)
|
||||
{
|
||||
this.idAvatarMap = idAvatarMap;
|
||||
this.idRelicMainPropMap = idRelicMainPropMap;
|
||||
this.idReliquaryAffixMap = idReliquaryAffixMap;
|
||||
this.idWeaponMap = idWeaponMap;
|
||||
this.reliqueryLevels = reliqueryLevels;
|
||||
this.reliquaries = reliquaries;
|
||||
this.avatarInfo = avatarInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建角色
|
||||
/// </summary>
|
||||
/// <returns>角色</returns>
|
||||
public PropertyAvatar CreateAvatar()
|
||||
{
|
||||
ReliquaryAndWeapon reliquaryAndWeapon = 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 = $"Lv.{avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value}",
|
||||
FetterLevel = avatarInfo.FetterInfo.ExpLevel,
|
||||
Weapon = reliquaryAndWeapon.Weapon,
|
||||
Reliquaries = reliquaryAndWeapon.Reliquaries,
|
||||
Constellations = SummaryHelper.CreateConstellations(avatarInfo.TalentIdList, avatar.SkillDepot.Talents),
|
||||
Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.GetCompositeSkillsNoInherents()),
|
||||
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
|
||||
Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"),
|
||||
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
|
||||
};
|
||||
}
|
||||
|
||||
private ReliquaryAndWeapon ProcessEquip(IList<Equip> equipments)
|
||||
{
|
||||
List<PropertyReliquary> reliquaryList = new();
|
||||
PropertyWeapon? weapon = null;
|
||||
|
||||
foreach (Equip equip in equipments)
|
||||
{
|
||||
switch (equip.Flat.ItemType)
|
||||
{
|
||||
case ItemType.ITEM_RELIQUARY:
|
||||
SummaryReliquaryFactory summaryReliquaryFactory = new(idReliquaryAffixMap, idRelicMainPropMap, reliqueryLevels, reliquaries, avatarInfo, equip);
|
||||
reliquaryList.Add(summaryReliquaryFactory.CreateReliquary());
|
||||
break;
|
||||
case ItemType.ITEM_WEAPON:
|
||||
weapon = CreateWeapon(equip);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new(reliquaryList, weapon!);
|
||||
}
|
||||
|
||||
private PropertyWeapon CreateWeapon(Equip equip)
|
||||
{
|
||||
MetadataWeapon weapon = idWeaponMap[equip.ItemId];
|
||||
|
||||
// AffixMap can be empty when it's a white weapon.
|
||||
KeyValuePair<string, int>? idLevel = equip.Weapon!.AffixMap?.Single();
|
||||
int affixLevel = idLevel.HasValue ? idLevel.Value.Value : 0;
|
||||
|
||||
WeaponStat mainStat = equip.Flat.WeaponStats![0];
|
||||
WeaponStat? subStat = equip.Flat.WeaponStats?.Count > 1 ? equip.Flat.WeaponStats![1] : null;
|
||||
|
||||
Pair<string, string> subProperty;
|
||||
if (subStat == null)
|
||||
{
|
||||
subProperty = new(string.Empty, string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
subStat.StatValue = subStat.StatValue - Math.Truncate(subStat.StatValue) > 0 ? subStat.StatValue / 100D : subStat.StatValue;
|
||||
subProperty = PropertyInfoDescriptor.FormatPair(subStat.AppendPropId, subStat.StatValue);
|
||||
}
|
||||
|
||||
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(mainStat.AppendPropId.GetDescription(), mainStat.StatValue.ToString()),
|
||||
|
||||
// Weapon
|
||||
SubProperty = subProperty,
|
||||
AffixLevel = $"精炼{affixLevel + 1}",
|
||||
AffixName = weapon.Affix?.Name ?? string.Empty,
|
||||
AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == affixLevel).Description ?? string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private struct ReliquaryAndWeapon
|
||||
{
|
||||
public List<PropertyReliquary> Reliquaries;
|
||||
public PropertyWeapon Weapon;
|
||||
|
||||
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon weapon)
|
||||
{
|
||||
Reliquaries = reliquaries;
|
||||
Weapon = weapon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Reliquary;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
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;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 简述工厂
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(ISummaryFactory))]
|
||||
internal class SummaryFactory : ISummaryFactory
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的简述工厂
|
||||
/// </summary>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
public SummaryFactory(IMetadataService metadataService)
|
||||
{
|
||||
this.metadataService = metadataService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos)
|
||||
{
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false);
|
||||
|
||||
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false);
|
||||
List<MetadataReliquary> reliquaries = await metadataService.GetReliquariesAsync().ConfigureAwait(false);
|
||||
|
||||
SummaryFactoryImplementation inner = new(idAvatarMap, idWeaponMap, idRelicMainPropMap, idReliquaryAffixMap, reliqueryLevels, reliquaries);
|
||||
return inner.Create(playerInfo, avatarInfos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Reliquary;
|
||||
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;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 真正实现
|
||||
/// </summary>
|
||||
internal class SummaryFactoryImplementation
|
||||
{
|
||||
private readonly Dictionary<int, MetadataAvatar> idAvatarMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
|
||||
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
/// <summary>
|
||||
/// 装配一个工厂实现
|
||||
/// </summary>
|
||||
/// <param name="idAvatarMap">角色映射</param>
|
||||
/// <param name="idWeaponMap">武器映射</param>
|
||||
/// <param name="idRelicMainPropMap">圣遗物主属性映射</param>
|
||||
/// <param name="idReliquaryAffixMap">圣遗物副词条映射</param>
|
||||
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
|
||||
/// <param name="reliquaries">圣遗物</param>
|
||||
public SummaryFactoryImplementation(
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap,
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> reliquaries)
|
||||
{
|
||||
this.idAvatarMap = idAvatarMap;
|
||||
this.idRelicMainPropMap = idRelicMainPropMap;
|
||||
this.idWeaponMap = idWeaponMap;
|
||||
this.reliqueryLevels = reliqueryLevels;
|
||||
this.reliquaries = reliquaries;
|
||||
this.idReliquaryAffixMap = idReliquaryAffixMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的属性统计对象
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">玩家信息</param>
|
||||
/// <param name="avatarInfos">角色信息</param>
|
||||
/// <returns>属性统计</returns>
|
||||
public Summary Create(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Player = SummaryHelper.CreatePlayer(playerInfo),
|
||||
Avatars = avatarInfos.Select(a =>
|
||||
{
|
||||
SummaryAvatarFactory summaryAvatarFactory = new(
|
||||
idAvatarMap,
|
||||
idWeaponMap,
|
||||
idRelicMainPropMap,
|
||||
idReliquaryAffixMap,
|
||||
reliqueryLevels,
|
||||
reliquaries,
|
||||
a);
|
||||
return summaryAvatarFactory.CreateAvatar();
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// 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 ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 简述帮助类
|
||||
/// </summary>
|
||||
internal static class SummaryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建玩家对象
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">玩家信息</param>
|
||||
/// <returns>玩家对象</returns>
|
||||
public static Player CreatePlayer(ModelPlayerInfo playerInfo)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Nickname = playerInfo.Nickname,
|
||||
Level = playerInfo.Level,
|
||||
Signature = playerInfo.Signature,
|
||||
FinishAchievementNumber = playerInfo.FinishAchievementNum,
|
||||
SipralAbyssFloorLevel = $"{playerInfo.TowerFloorIndex} - {playerInfo.TowerLevelIndex}",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建命之座
|
||||
/// </summary>
|
||||
/// <param name="talentIds">激活的命座列表</param>
|
||||
/// <param name="talents">全部命座</param>
|
||||
/// <returns>命之座</returns>
|
||||
public static List<Constellation> CreateConstellations(IList<int>? talentIds, IList<SkillBase> talents)
|
||||
{
|
||||
return talents.Select(talent => new Constellation()
|
||||
{
|
||||
Name = talent.Name,
|
||||
Icon = SkillIconConverter.IconNameToUri(talent.Icon),
|
||||
Description = talent.Description,
|
||||
IsActiviated = talentIds?.Contains(talent.Id) ?? false,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建技能组
|
||||
/// </summary>
|
||||
/// <param name="skillLevelMap">技能等级映射</param>
|
||||
/// <param name="proudSkillExtraLevelMap">额外提升等级映射</param>
|
||||
/// <param name="proudSkills">技能列表</param>
|
||||
/// <returns>技能</returns>
|
||||
public static List<Skill> CreateSkills(IDictionary<string, int> skillLevelMap, IDictionary<string, int>? proudSkillExtraLevelMap, IEnumerable<ProudableSkill> proudSkills)
|
||||
{
|
||||
Dictionary<string, int> 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<Skill> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建角色属性
|
||||
/// </summary>
|
||||
/// <param name="fightPropMap">属性映射</param>
|
||||
/// <returns>列表</returns>
|
||||
public static List<Pair2<string, string, string?>> CreateAvatarProperties(IDictionary<FightProperty, double> fightPropMap)
|
||||
{
|
||||
List<Pair2<string, string, string?>> properties;
|
||||
|
||||
double baseHp = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_BASE_HP); // 1
|
||||
double hp = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_HP); // 2
|
||||
double hpPercent = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_HP_PERCENT); // 3
|
||||
double hpAdd = hp + (baseHp * hpPercent);
|
||||
double maxHp = baseHp + hpAdd;
|
||||
Pair2<string, string, string?> hpPair2 = PropertyInfoDescriptor.FormatIntegerPair2("生命值", FormatMethod.Integer, maxHp, hpAdd);
|
||||
|
||||
double baseAtk = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_BASE_ATTACK); // 4
|
||||
double atk = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ATTACK); // 5
|
||||
double atkPrecent = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6
|
||||
double atkAdd = atk + (baseAtk * atkPrecent);
|
||||
double maxAtk = baseAtk + atkAdd;
|
||||
Pair2<string, string, string?> atkPair2 = PropertyInfoDescriptor.FormatIntegerPair2("攻击力", FormatMethod.Integer, maxAtk, atkAdd);
|
||||
|
||||
double baseDef = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_BASE_DEFENSE); // 7
|
||||
double def = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_DEFENSE); // 8
|
||||
double defPercent = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9
|
||||
double defAdd = def + (baseDef * defPercent);
|
||||
double maxDef = baseDef + defPercent;
|
||||
Pair2<string, string, string?> defPair2 = PropertyInfoDescriptor.FormatIntegerPair2("防御力", FormatMethod.Integer, maxDef, defAdd);
|
||||
|
||||
double em = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28
|
||||
Pair2<string, string, string?> emPair2 = PropertyInfoDescriptor.FormatIntegerPair2("元素精通", FormatMethod.Integer, em);
|
||||
|
||||
double critRate = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CRITICAL); // 20
|
||||
Pair2<string, string, string?> critRatePair2 = PropertyInfoDescriptor.FormatIntegerPair2("暴击率", FormatMethod.Percent, critRate);
|
||||
|
||||
double critDMG = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22
|
||||
Pair2<string, string, string?> critDMGPair2 = PropertyInfoDescriptor.FormatIntegerPair2("暴击伤害", FormatMethod.Percent, critDMG);
|
||||
|
||||
double chargeEff = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23
|
||||
Pair2<string, string, string?> chargeEffPair2 = PropertyInfoDescriptor.FormatIntegerPair2("元素充能效率", FormatMethod.Percent, chargeEff);
|
||||
|
||||
properties = new() { hpPair2, atkPair2, defPair2, emPair2, critRatePair2, critDMGPair2, chargeEffPair2 };
|
||||
|
||||
FightProperty bonusProperty = GetBonusFightProperty(fightPropMap);
|
||||
if (bonusProperty != FightProperty.FIGHT_PROP_NONE)
|
||||
{
|
||||
double value = fightPropMap[bonusProperty];
|
||||
if (value > 0)
|
||||
{
|
||||
Pair2<string, string, string?> bonusPair2 = new(bonusProperty.GetDescription(), PropertyInfoDescriptor.FormatValue(FormatMethod.Percent, value), null);
|
||||
properties.Add(bonusPair2);
|
||||
}
|
||||
}
|
||||
|
||||
// 物伤
|
||||
if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT))
|
||||
{
|
||||
double value = fightPropMap[FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT];
|
||||
if (value > 0)
|
||||
{
|
||||
string description = FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT.GetDescription();
|
||||
Pair2<string, string, string?> physicalBonusPair2 = new(description, PropertyInfoDescriptor.FormatValue(FormatMethod.Percent, value), null);
|
||||
properties.Add(physicalBonusPair2);
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取副属性对应的最大属性的Id
|
||||
/// </summary>
|
||||
/// <param name="appendId">属性Id</param>
|
||||
/// <returns>最大属性Id</returns>
|
||||
public static int GetAffixMaxId(int appendId)
|
||||
{
|
||||
int value = appendId / 100000;
|
||||
int max = value switch
|
||||
{
|
||||
1 => 2,
|
||||
2 => 3,
|
||||
3 or 4 or 5 => 4,
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
|
||||
return (appendId / 10 * 10) + max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取百分比属性副词条分数
|
||||
/// </summary>
|
||||
/// <param name="appendId">id</param>
|
||||
/// <returns>分数</returns>
|
||||
public static double GetPercentSubAffixScore(int appendId)
|
||||
{
|
||||
int maxId = GetAffixMaxId(appendId);
|
||||
int delta = maxId - appendId;
|
||||
|
||||
return (maxId / 100000, delta) switch
|
||||
{
|
||||
(5, 0) => 100,
|
||||
(5, 1) => 90,
|
||||
(5, 2) => 80,
|
||||
(5, 3) => 70,
|
||||
|
||||
(4, 0) => 100,
|
||||
(4, 1) => 90,
|
||||
(4, 2) => 80,
|
||||
(4, 3) => 70,
|
||||
|
||||
(3, 0) => 100,
|
||||
(3, 1) => 85,
|
||||
(3, 2) => 70,
|
||||
|
||||
(2, 0) => 100,
|
||||
(2, 1) => 80,
|
||||
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取双爆评分
|
||||
/// </summary>
|
||||
/// <param name="fightPropMap">属性</param>
|
||||
/// <returns>评分</returns>
|
||||
public static double ScoreCrit(IDictionary<FightProperty, double> fightPropMap)
|
||||
{
|
||||
double cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL];
|
||||
double cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT];
|
||||
|
||||
return 100 * ((cr * 2) + cd);
|
||||
}
|
||||
|
||||
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<FightProperty, double> 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;
|
||||
}
|
||||
|
||||
return FightProperty.FIGHT_PROP_NONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
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.Web.Enka.Model;
|
||||
using System.Runtime.InteropServices;
|
||||
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
|
||||
using MetadataReliquaryAffix = Snap.Hutao.Model.Metadata.Reliquary.ReliquaryAffix;
|
||||
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
|
||||
using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物工厂
|
||||
/// </summary>
|
||||
internal class SummaryReliquaryFactory
|
||||
{
|
||||
private readonly Dictionary<int, MetadataReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
private readonly ModelAvatarInfo avatarInfo;
|
||||
private readonly Equip equip;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的圣遗物工厂
|
||||
/// </summary>
|
||||
/// <param name="idReliquaryAffixMap">圣遗物副词条映射</param>
|
||||
/// <param name="idRelicMainPropMap">圣遗物主属性映射</param>
|
||||
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
|
||||
/// <param name="reliquaries">圣遗物列表</param>
|
||||
/// <param name="avatarInfo">角色信息</param>
|
||||
/// <param name="equip">圣遗物</param>
|
||||
public SummaryReliquaryFactory(
|
||||
Dictionary<int, MetadataReliquaryAffix> idReliquaryAffixMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> reliquaries,
|
||||
ModelAvatarInfo avatarInfo,
|
||||
Equip equip)
|
||||
{
|
||||
this.idReliquaryAffixMap = idReliquaryAffixMap;
|
||||
this.idRelicMainPropMap = idRelicMainPropMap;
|
||||
this.reliqueryLevels = reliqueryLevels;
|
||||
this.reliquaries = reliquaries;
|
||||
|
||||
this.avatarInfo = avatarInfo;
|
||||
this.equip = equip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造圣遗物
|
||||
/// </summary>
|
||||
/// <returns>圣遗物</returns>
|
||||
public PropertyReliquary CreateReliquary()
|
||||
{
|
||||
MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
|
||||
List<ReliquarySubProperty> subProperty = equip.Reliquary!.AppendPropIdList.Select(id => CreateSubProperty(id)).ToList();
|
||||
|
||||
int affixCount = GetAffixCount(reliquary);
|
||||
Span<ReliquarySubProperty> span = CollectionsMarshal.AsSpan(subProperty);
|
||||
List<ReliquarySubProperty> primary = new(span[..^affixCount].ToArray());
|
||||
List<ReliquarySubProperty> secondary = new(span[^affixCount..].ToArray());
|
||||
|
||||
ReliquaryLevel relicLevel = reliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel);
|
||||
FightProperty property = idRelicMainPropMap[equip.Reliquary.MainPropId];
|
||||
|
||||
return new()
|
||||
{
|
||||
// NameIconDescription
|
||||
Name = reliquary.Name,
|
||||
Icon = RelicIconConverter.IconNameToUri(reliquary.Icon),
|
||||
Description = reliquary.Description,
|
||||
|
||||
// EquipBase
|
||||
Level = $"+{equip.Reliquary.Level - 1}",
|
||||
Quality = reliquary.RankLevel,
|
||||
MainProperty = new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, relicLevel.Properties[property])),
|
||||
|
||||
// Reliquary
|
||||
// SubProperties = subProperty,
|
||||
PrimarySubProperties = primary,
|
||||
SecondarySubProperties = secondary,
|
||||
Score = ScoreReliquary(property, reliquary, relicLevel, subProperty),
|
||||
};
|
||||
}
|
||||
|
||||
private int GetAffixCount(MetadataReliquary reliquary)
|
||||
{
|
||||
return (reliquary.RankLevel, equip.Reliquary!.Level) switch
|
||||
{
|
||||
(ItemQuality.QUALITY_ORANGE, > 20) => 5,
|
||||
(ItemQuality.QUALITY_ORANGE, > 16) => 4,
|
||||
(ItemQuality.QUALITY_ORANGE, > 12) => 3,
|
||||
(ItemQuality.QUALITY_ORANGE, > 8) => 2,
|
||||
(ItemQuality.QUALITY_ORANGE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_ORANGE, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_PURPLE, > 16) => 4,
|
||||
(ItemQuality.QUALITY_PURPLE, > 12) => 3,
|
||||
(ItemQuality.QUALITY_PURPLE, > 8) => 2,
|
||||
(ItemQuality.QUALITY_PURPLE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_PURPLE, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_BLUE, > 12) => 3,
|
||||
(ItemQuality.QUALITY_BLUE, > 8) => 2,
|
||||
(ItemQuality.QUALITY_BLUE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_BLUE, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_GREEN, > 4) => 1,
|
||||
(ItemQuality.QUALITY_GREEN, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_WHITE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_WHITE, _) => 0,
|
||||
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private double ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List<ReliquarySubProperty> subProperties)
|
||||
{
|
||||
// 沙 杯 头
|
||||
if (equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS)
|
||||
{
|
||||
AffixWeight weightConfig = GetAffixWeightForAvatarId();
|
||||
ReliquaryLevel maxRelicLevel = reliqueryLevels.Where(r => r.Quality == reliquary.RankLevel).MaxBy(r => r.Level)!;
|
||||
|
||||
double percent = relicLevel.Properties[property] / maxRelicLevel.Properties[property];
|
||||
double baseScore = 8 * percent * weightConfig.GetValueOrDefault(property, 0);
|
||||
|
||||
double score = subProperties.Sum(p => p.Score);
|
||||
return ((score + baseScore) / 1700) * 66;
|
||||
}
|
||||
else
|
||||
{
|
||||
double score = subProperties.Sum(p => p.Score);
|
||||
return (score / 900) * 66;
|
||||
}
|
||||
}
|
||||
|
||||
private AffixWeight GetAffixWeightForAvatarId()
|
||||
{
|
||||
return ReliquaryWeightConfiguration.AffixWeights.FirstOrDefault(w => w.AvatarId == avatarInfo.AvatarId, ReliquaryWeightConfiguration.Default);
|
||||
}
|
||||
|
||||
private ReliquarySubProperty CreateSubProperty(int appendPropId)
|
||||
{
|
||||
MetadataReliquaryAffix affix = idReliquaryAffixMap[appendPropId];
|
||||
FightProperty property = affix.Type;
|
||||
|
||||
double score = ScoreSubAffix(appendPropId);
|
||||
return new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, affix.Value), score);
|
||||
}
|
||||
|
||||
private double ScoreSubAffix(int appendId)
|
||||
{
|
||||
MetadataReliquaryAffix affix = idReliquaryAffixMap[appendId];
|
||||
|
||||
AffixWeight weightConfig = GetAffixWeightForAvatarId();
|
||||
double weight = weightConfig.GetValueOrDefault(affix.Type, 0) / 100D;
|
||||
|
||||
// 小字词条,转换到等效百分比计算
|
||||
if (affix.Type is FightProperty.FIGHT_PROP_HP or FightProperty.FIGHT_PROP_ATTACK or FightProperty.FIGHT_PROP_DEFENSE)
|
||||
{
|
||||
// 等效百分比 [ 当前小字词条 / 角色基本属性 ]
|
||||
double equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1];
|
||||
|
||||
// 获取对应百分比词条权重
|
||||
weight = weightConfig.GetValueOrDefault(affix.Type + 1, 0) / 100D;
|
||||
|
||||
// 最大同属性百分比数值 最大同属性百分比Id 第四五位是战斗属性位
|
||||
MetadataReliquaryAffix maxPercentAffix = idReliquaryAffixMap[SummaryHelper.GetAffixMaxId(appendId + 10)];
|
||||
double equalScore = equalPercent / maxPercentAffix.Value;
|
||||
|
||||
return weight * equalScore * 100;
|
||||
}
|
||||
|
||||
return weight * SummaryHelper.GetPercentSubAffixScore(appendId);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息服务
|
||||
/// </summary>
|
||||
internal interface IAvatarInfoService
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步获取总览数据
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="refreshOption">刷新选项</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>总览数据</returns>
|
||||
Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新选项
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum RefreshOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否存入本地数据库
|
||||
/// </summary>
|
||||
StoreInDatabase = 0b00000001,
|
||||
|
||||
/// <summary>
|
||||
/// 从API获取
|
||||
/// </summary>
|
||||
RequestFromAPI = 0b00000010,
|
||||
|
||||
/// <summary>
|
||||
/// 仅数据库
|
||||
/// </summary>
|
||||
DatabaseOnly = 0b00000000,
|
||||
|
||||
/// <summary>
|
||||
/// 标准操作
|
||||
/// </summary>
|
||||
Standard = StoreInDatabase | RequestFromAPI,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新结果
|
||||
/// </summary>
|
||||
public enum RefreshResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 正常
|
||||
/// </summary>
|
||||
Ok,
|
||||
|
||||
/// <summary>
|
||||
/// 元数据加载失败
|
||||
/// </summary>
|
||||
MetadataUninitialized,
|
||||
|
||||
/// <summary>
|
||||
/// API 不可用
|
||||
/// </summary>
|
||||
APIUnavailable,
|
||||
|
||||
/// <summary>
|
||||
/// 角色橱窗未对外开放
|
||||
/// </summary>
|
||||
ShowcaseNotOpen,
|
||||
}
|
||||
@@ -5,8 +5,9 @@ using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
|
||||
@@ -34,35 +35,28 @@ public static class GachaStatisticsExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// 完成添加
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
|
||||
where TKey : notnull
|
||||
/// <param name="summaryItems">简述物品列表</param>
|
||||
public static void CompleteAdding(this List<SummaryItem> summaryItems)
|
||||
{
|
||||
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
|
||||
}
|
||||
// we can't trust first item's prev state.
|
||||
bool isPreviousUp = true;
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>是否存在键值</returns>
|
||||
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
|
||||
where TKey : notnull
|
||||
{
|
||||
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
|
||||
if (!Unsafe.IsNullRef(ref value))
|
||||
// mark the IsGuarentee
|
||||
foreach (SummaryItem item in summaryItems)
|
||||
{
|
||||
++value;
|
||||
return true;
|
||||
if (item.IsUp && (!isPreviousUp))
|
||||
{
|
||||
item.IsGuarentee = true;
|
||||
}
|
||||
|
||||
isPreviousUp = item.IsUp;
|
||||
item.Color = GetColorByName(item.Name);
|
||||
}
|
||||
|
||||
return false;
|
||||
// reverse items
|
||||
summaryItems.Reverse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,4 +97,14 @@ public static class GachaStatisticsExtensions
|
||||
.OrderByDescending(item => item.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Color GetColorByName(string name)
|
||||
{
|
||||
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
|
||||
Span<byte> first = new(codes, 0, 5);
|
||||
Span<byte> second = new(codes, 5, 5);
|
||||
Span<byte> third = new(codes, 10, 5);
|
||||
Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average());
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user