Compare commits

...

17 Commits

Author SHA1 Message Date
DismissedLight
55f16a6357 avatar info 2022-10-10 14:26:40 +08:00
DismissedLight
560068ca20 Merge pull request #90 from DGP-Studio/winui-120
update to was 1.2.0 preview
2022-10-06 11:59:52 +08:00
DismissedLight
2f1981108e update to was 1.2.0 preview 2022-10-06 11:59:17 +08:00
DismissedLight
b545c0d09b fix #88 2022-10-06 11:29:07 +08:00
DismissedLight
009feced08 Merge pull request #85 from Masterain98/main
Update bug_report.yml
2022-10-04 23:53:13 +08:00
Masterain
fa7fcbc9cc Update bug_report.yml 2022-10-04 06:03:38 -07:00
DismissedLight
d28624dea1 Merge pull request #84 from DGP-Studio/feat/avatar_info
Feat/avatar info
2022-10-04 16:48:22 +08:00
DismissedLight
d802d1af15 avatar info complete 1 2022-10-04 16:44:53 +08:00
DismissedLight
43e3df9cba avatar info phase 1 2022-10-04 00:09:25 +08:00
DismissedLight
8e5e59ad0d remove null check to cookie 2022-09-29 16:33:01 +08:00
DismissedLight
ed7d55ddd5 Merge pull request #64 from DGP-Studio/refactor/userserive
UserService v2 implementation
2022-09-29 16:14:27 +08:00
DismissedLight
94ef94a621 impl 2022-09-29 16:13:47 +08:00
Masterain
fd35213741 Merge pull request #52 from Masterain98/main
Update README.md
2022-09-27 19:23:15 -07:00
Masterain
0f752129b7 Update README.md 2022-09-27 19:20:49 -07:00
DismissedLight
331cc14532 Update Persistence.cs 2022-09-27 14:38:06 +08:00
DismissedLight
c0ddb24825 fix window issue 2022-09-27 14:36:13 +08:00
DismissedLight
e925c5909c update readme 2022-09-27 12:35:37 +08:00
145 changed files with 4201 additions and 831 deletions

View File

@@ -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

View File

@@ -3,10 +3,18 @@
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
## 项目首页(文档)
[![Deploy Docs](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml)
[HUT.AO](https://hut.ao)
## 安装
* 前往 [下载页面](https://go.hut.ao/archive) 下载最新版本的 `胡桃` 安装包
* 完全解压后,使用 powershell 运行 `install.ps1` 文件
* 前往 [下载页面](https://go.hut.ao/down) 下载最新版本的 `胡桃` 安装包
* (曾启用的可以跳过此步骤)在系统设置中打开 **开发者选项** 界面,勾选 `开发人员模式``允许 PowerShell 脚本`
* 完全解压后,右键使用 powershell 运行 `install.ps1` 文件
* 安装完成后可以关闭 `允许 PowerShell 脚本`
## 特别感谢

View File

@@ -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]
#### 命名样式 ####

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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 = ((IDictionary<string, Type>)TranslationMap).GetValueOrDefault2(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}
/// <summary>

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <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);
}
}

View 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="&#xE8FD;"/>
</SplitButton.Content>
<SplitButton.Flyout>
<MenuFlyout>
<RadioMenuFlyoutItem
Tag="List"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Text="列表"/>
<RadioMenuFlyoutItem
Tag="Grid"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Text="网格"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</UserControl>

View File

@@ -0,0 +1,82 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Web.Hutao.Model;
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 { return (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;
}
}

View File

@@ -102,7 +102,7 @@ public class DescriptionTextBlock : ContentControl
if (i == description.Length - 1)
{
AppendText(text, description[last..i]);
AppendText(text, description[last..(i + 1)]);
}
}
}

View File

@@ -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);

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 20)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";

View File

@@ -22,7 +22,7 @@ internal static class CoreEnvironment
/// <summary>
/// 动态密钥2的盐
/// </summary>
public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk";
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
/// <summary>
/// 米游社请求UA

View File

@@ -74,3 +74,80 @@ internal class DbCurrent<TEntity, TMessage>
}
}
}
/// <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);
}
}
}

View File

@@ -2,7 +2,10 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using System.Diagnostics;
using System.IO;
namespace Snap.Hutao.Core.Exception;
@@ -26,22 +29,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);
}

View File

@@ -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();
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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());
}
}

View File

@@ -4,6 +4,7 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Win32;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32.Foundation;
@@ -25,11 +26,15 @@ internal static class Persistence
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
RectInt32 rect = new(0, 0, size.Width, size.Height);
RectInt32 rect = StructMarshal.RectInt32(size);
// Make it centralized
TransformToCenterScreen(ref rect);
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (target.Width * target.Height < 848 * 524)
{
target = rect;
}
TransformToCenterScreen(ref target);
appWindow.MoveAndResize(target);
}

View File

@@ -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;
@@ -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>

View File

@@ -8,6 +8,17 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class NumberExtensions
{
/// <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>

View File

@@ -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));
}
}
}

View File

@@ -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
{

View File

@@ -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; }
}

View File

@@ -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!;
}

View File

@@ -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; }
}

View File

@@ -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!;
}

View File

@@ -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!;
}

View File

@@ -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!;
}

View File

@@ -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 class Reliquary : EquipBase
{
/// <summary>
/// 副属性列表
/// </summary>
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
/// <summary>
/// 评分
/// </summary>
public double Score { get; set; }
/// <summary>
/// 格式化评分
/// </summary>
public string ScoreFormatted { get => $"{Score:F2}"; }
}

View File

@@ -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; }
}

View File

@@ -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!;
}

View File

@@ -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!;
}

View File

@@ -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!;
}

View File

@@ -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> ParseCookie(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,95 +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> TryUpgradeAsync(IDictionary<string, string> addition, AuthClient authClient, CancellationToken token)
{
IDictionary<string, string> cookie = ParseCookie(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 TryAddStokenToCookieAsync(cookie, authClient, token).ConfigureAwait(false);
if (result)
{
Cookie = ToCookieString(cookie);
}
return result;
}
private static string ToCookieString(IDictionary<string, string> cookie)
{
return string.Join(';', cookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
private static async Task<bool> TryAddStokenToCookieAsync(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)
@@ -206,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 TryAddStokenToCookieAsync(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)

View File

@@ -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 };
}
}

View File

@@ -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));
}
}

View File

@@ -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 };
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -15,7 +15,7 @@ public enum ItemType
ITEM_NONE = 0,
/// <summary>
/// 贵重道具
/// 虚拟道具
/// </summary>
ITEM_VIRTUAL = 1,

View File

@@ -21,6 +21,7 @@ public enum WeaponType
WEAPON_SWORD_ONE_HAND = 1,
#region Not Used
/// <summary>
/// ?
/// </summary>

View File

@@ -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);
}

View File

@@ -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

View File

@@ -0,0 +1,67 @@
// 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 Cyno = 10000071;
public const int Candace = 10000072;
}

View File

@@ -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; }
}

View File

@@ -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)

View File

@@ -12,24 +12,43 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal sealed class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
{
/// <inheritdoc/>
public override IList<LevelParam<string, ParameterInfo>> Convert(DescParam rawDescParam)
/// <summary>
/// 获取特定等级的解释
/// </summary>
/// <param name="from">源</param>
/// <param name="level">等级</param>
/// <returns>特定等级的解释</returns>
public static LevelParam<string, ParameterInfo> Convert(DescParam from, int level)
{
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
.Select(param => new LevelParam<string, ParameterInfo>(
param.Level.ToString(),
GetParameterInfos(rawDescParam, param.Parameters)))
// 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(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++)

View File

@@ -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 });
}

View File

@@ -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),
};
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -18,7 +18,7 @@ public class Reliquary
/// <summary>
/// 允许出现的等级
/// </summary>
public IEnumerable<ItemQuality> RankLevels { get; set; } = default!;
public ItemQuality RankLevel { get; set; } = default!;
/// <summary>
/// 套装Id

View File

@@ -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!;
}

View 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!;
}

View 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>
/// <typeparam name="TLevel">等级的类型</typeparam>
public class LevelDescription<TLevel>
{
/// <summary>
/// 等级
/// </summary>
public TLevel Level { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
}

View File

@@ -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,6 +54,11 @@ 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;

View 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; }
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.3.0" />
Version="1.1.9.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,148 @@
// 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)
{
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();
}
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -0,0 +1,79 @@
// 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 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.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 } },
};
}

View File

@@ -0,0 +1,165 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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(),
};
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,145 @@
// 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 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();
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,
Score = ScoreReliquary(property, reliquary, relicLevel, subProperty),
};
}
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(avatarInfo.AvatarId);
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[property];
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(int avatarId)
{
return ReliquaryWeightConfiguration.AffixWeights.First(w => w.AvatarId == avatarId);
}
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(avatarInfo.AvatarId);
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);
}
}

View File

@@ -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);
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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;
}
}

View File

@@ -89,9 +89,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.TrackAvatar(item, avatar, isUp);
avatarWishBuilder.TrackAvatar(item, avatar, isUp);
weaponWishBuilder.TrackAvatar(item, avatar, isUp);
permanentWishBuilder.Track(item, avatar, isUp);
avatarWishBuilder.Track(item, avatar, isUp);
weaponWishBuilder.Track(item, avatar, isUp);
}
// It's a weapon
@@ -116,9 +116,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.TrackWeapon(item, weapon, isUp);
avatarWishBuilder.TrackWeapon(item, weapon, isUp);
weaponWishBuilder.TrackWeapon(item, weapon, isUp);
permanentWishBuilder.Track(item, weapon, isUp);
avatarWishBuilder.Track(item, weapon, isUp);
weaponWishBuilder.Track(item, weapon, isUp);
}
else
{

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Abstraction;

View File

@@ -5,12 +5,8 @@ using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Security.Cryptography;
using System.Text;
using Windows.UI;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -75,9 +71,9 @@ internal class TypedWishSummaryBuilder
/// 追踪物品
/// </summary>
/// <param name="item">祈愿物品</param>
/// <param name="avatar">对应角色</param>
/// <param name="source">对应武器</param>
/// <param name="isUp">是否为Up物品</param>
public void TrackAvatar(GachaItem item, Avatar avatar, bool isUp)
public void Track(GachaItem item, ISummaryItemSource source, bool isUp)
{
if (typeEvaluator(item.GachaType))
{
@@ -89,7 +85,7 @@ internal class TypedWishSummaryBuilder
++totalCountTracker;
TrackFromToTime(item.Time);
switch (avatar.Quality)
switch (source.Quality)
{
case ItemQuality.QUALITY_ORANGE:
{
@@ -102,55 +98,7 @@ internal class TypedWishSummaryBuilder
lastUpOrangePullTracker = 0;
}
summaryItemCache.Add(avatar.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
lastOrangePullTracker = 0;
++totalOrangePullTracker;
break;
}
case ItemQuality.QUALITY_PURPLE:
{
lastPurplePullTracker = 0;
++totalPurplePullTracker;
break;
}
}
}
}
/// <summary>
/// 追踪物品
/// </summary>
/// <param name="item">祈愿物品</param>
/// <param name="weapon">对应武器</param>
/// <param name="isUp">是否为Up物品</param>
public void TrackWeapon(GachaItem item, Weapon weapon, bool isUp)
{
if (typeEvaluator(item.GachaType))
{
++lastOrangePullTracker;
++lastPurplePullTracker;
++lastUpOrangePullTracker;
// track total pulls
++totalCountTracker;
TrackFromToTime(item.Time);
switch (weapon.RankLevel)
{
case ItemQuality.QUALITY_ORANGE:
{
TrackMinMaxOrangePull(lastOrangePullTracker);
averageOrangePullTracker.Add(lastOrangePullTracker);
if (isUp)
{
averageUpOrangePullTracker.Add(lastUpOrangePullTracker);
lastUpOrangePullTracker = 0;
}
summaryItemCache.Add(weapon.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
summaryItemCache.Add(source.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
lastOrangePullTracker = 0;
++totalOrangePullTracker;
@@ -179,7 +127,7 @@ internal class TypedWishSummaryBuilder
/// <returns>类型化祈愿统计信息</returns>
public TypedWishSummary ToTypedWishSummary()
{
CompleteSummaryItems(summaryItemCache);
summaryItemCache.CompleteAdding();
double totalCountDouble = totalCountTracker;
return new()
@@ -209,37 +157,6 @@ internal class TypedWishSummaryBuilder
};
}
private static void CompleteSummaryItems(List<SummaryItem> summaryItems)
{
// we can't trust first item's prev state.
bool isPreviousUp = true;
// mark the IsGuarentee
foreach (SummaryItem item in summaryItems)
{
if (item.IsUp && (!isPreviousUp))
{
item.IsGuarentee = true;
}
isPreviousUp = item.IsUp;
item.Color = GetColorByName(item.Name);
}
// reverse items
summaryItems.Reverse();
}
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()/*.HalfRange()*/, second.Average()/*.HalfRange()*/, third.Average()/*.HalfRange()*/);
return color;
}
private void TrackMinMaxOrangePull(int lastOrangePull)
{
if (lastOrangePull < minOrangePullTracker || minOrangePullTracker == 0)

View File

@@ -5,6 +5,8 @@ using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
@@ -45,6 +47,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService;
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
private readonly ILogger<GachaLogService> logger;
private readonly DbCurrent<GachaArchive, Message.GachaArchiveChangedMessage> dbCurrent;
private readonly Dictionary<string, ItemBase> itemBaseCache = new();
@@ -65,6 +68,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
/// <param name="metadataService">元数据服务</param>
/// <param name="infoBarService">信息条服务</param>
/// <param name="gachaStatisticsFactory">祈愿统计工厂</param>
/// <param name="logger">日志器</param>
/// <param name="messenger">消息器</param>
public GachaLogService(
AppDbContext appDbContext,
@@ -73,6 +77,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
IMetadataService metadataService,
IInfoBarService infoBarService,
IGachaStatisticsFactory gachaStatisticsFactory,
ILogger<GachaLogService> logger,
IMessenger messenger)
{
this.appDbContext = appDbContext;
@@ -80,6 +85,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
this.gachaInfoClient = gachaInfoClient;
this.metadataService = metadataService;
this.infoBarService = infoBarService;
this.logger = logger;
this.gachaStatisticsFactory = gachaStatisticsFactory;
dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger);
@@ -143,21 +149,24 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
}
/// <inheritdoc/>
public Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive = null)
public async Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive = null)
{
archive ??= CurrentArchive;
// Return statistics
if (archive != null)
{
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
IQueryable<GachaItem> items = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId);
return gachaStatisticsFactory.CreateAsync(items);
GachaStatistics statistics = await gachaStatisticsFactory.CreateAsync(items).ConfigureAwait(false);
logger.LogInformation(EventIds.GachaStatisticGeneration, "GachaStatistic Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds);
return statistics;
}
else
{
return Must.Fault<GachaStatistics>("没有选中的存档");
throw Must.NeverHappen();
}
}

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
@@ -35,10 +35,10 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
Model.Binding.User? user = userService.CurrentUser;
Model.Binding.User? user = userService.Current;
if (user != null)
{
if (user.Cookie!.Contains(CookieKeys.STOKEN) && user.SelectedUserGameRole != null)
if (user.Cookie!.ContainsSToken() && user.SelectedUserGameRole != null)
{
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
@@ -51,6 +51,6 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
}
}
return new(false, null!);
return new(false, "当前用户的Cookie不包含 Stoken");
}
}

View File

@@ -18,6 +18,7 @@ internal class RegistryLauncherLocator : IGameLocator
/// <inheritdoc/>
public Task<ValueResult<bool, string>> LocateGamePathAsync()
{
// TODO: fix folder moved issue
return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe"));
}

View File

@@ -91,19 +91,18 @@ internal class InfoBarService : IInfoBarService
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay);
}
[SuppressMessage("", "VSTHRD100")]
private async void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
private void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
{
if (infoBarStack is null)
{
return;
}
await PrepareInfoBarAndShowInternalAsync(severity, title, message, delay).ConfigureAwait(false);
PrepareInfoBarAndShowInternalAsync(severity, title, message, delay).SafeForget();
}
/// <summary>
/// 此方法应在主线程上运行
/// 准备信息条并显示
/// </summary>
/// <param name="severity">严重程度</param>
/// <param name="title">标题</param>
@@ -122,11 +121,12 @@ internal class InfoBarService : IInfoBarService
};
infoBar.Closed += OnInfoBarClosed;
Must.NotNull(infoBarStack!).Children.Add(infoBar);
infoBarStack!.Children.Add(infoBar);
if (delay > 0)
{
await Task.Delay(delay);
await Task.Delay(delay).ConfigureAwait(true);
infoBarStack.Children.Remove(infoBar);
infoBar.IsOpen = false;
}
}
@@ -136,7 +136,7 @@ internal class InfoBarService : IInfoBarService
{
await ThreadHelper.SwitchToMainThreadAsync();
Must.NotNull(infoBarStack!).Children.Remove(sender);
infoBarStack!.Children.Remove(sender);
sender.Closed -= OnInfoBarClosed;
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
@@ -47,21 +48,35 @@ internal interface IMetadataService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>卡池配置列表</returns>
ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default(CancellationToken));
ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default);
/// <summary>
/// 异步获取Id到角色的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Id到角色的字典</returns>
ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default(CancellationToken));
ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取ID到圣遗物副词条的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>字典</returns>
ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取圣遗物主词条Id与属性的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>字典</returns>
ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取ID到武器的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Id到武器的字典</returns>
ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default(CancellationToken));
ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取名称到角色的字典
@@ -91,6 +106,13 @@ internal interface IMetadataService
/// <returns>圣遗物强化属性列表</returns>
ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default);
/// <summary>
/// 异步获取圣遗物等级数据
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>圣遗物等级数据</returns>
ValueTask<List<ReliquaryLevel>> GetReliquaryLevelsAsync(CancellationToken token = default);
/// <summary>
/// 异步获取圣遗物主属性强化属性列表
/// </summary>

View File

@@ -0,0 +1,107 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Metadata.Weapon;
namespace Snap.Hutao.Service.Metadata;
/// <summary>
/// 接口实现部分
/// </summary>
internal partial class MetadataService
{
/// <inheritdoc/>
public ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<AchievementGoal>>("AchievementGoal", token);
}
/// <inheritdoc/>
public ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
}
/// <inheritdoc/>
public ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Avatar>>("Avatar", token);
}
/// <inheritdoc/>
public ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<GachaEvent>>("GachaEvent", token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
}
/// <inheritdoc/>
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Reliquary>>("Reliquary", token);
}
/// <inheritdoc/>
public ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
}
/// <inheritdoc/>
public ValueTask<List<ReliquaryLevel>> GetReliquaryLevelsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<ReliquaryLevel>>("ReliquaryMainAffixLevel", token);
}
/// <inheritdoc/>
public ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
}
/// <inheritdoc/>
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Weapon>>("Weapon", token);
}
}

View File

@@ -8,11 +8,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
@@ -26,7 +21,7 @@ namespace Snap.Hutao.Service.Metadata;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IMetadataService))]
[HttpClient(HttpClientConfigration.Default)]
internal class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization
internal partial class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization
{
private const string MetaAPIHost = "http://hutao-metadata.snapgenshin.com";
private const string MetaFileName = "Meta.json";
@@ -92,78 +87,6 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed in {time}ms", stopwatch.GetElapsedTime().TotalMilliseconds);
}
/// <inheritdoc/>
public ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<AchievementGoal>>("AchievementGoal", token);
}
/// <inheritdoc/>
public ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
}
/// <inheritdoc/>
public ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Avatar>>("Avatar", token);
}
/// <inheritdoc/>
public ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<GachaEvent>>("GachaEvent", token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
}
/// <inheritdoc/>
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Reliquary>>("Reliquary", token);
}
/// <inheritdoc/>
public ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
}
/// <inheritdoc/>
public ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
}
/// <inheritdoc/>
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<Weapon>>("Weapon", token);
}
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token)
{
IDictionary<string, string>? metaMd5Map = null;
@@ -295,4 +218,20 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
Dictionary<TKey, TValue> dict = list.ToDictionaryOverride(keySelector);
return memoryCache.Set(cacheKey, dict);
}
private async ValueTask<Dictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue, TData>(string fileName, Func<TData, TKey> keySelector, Func<TData, TValue> valueSelector, CancellationToken token)
where TKey : notnull
{
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}.{typeof(TValue).Name}";
if (memoryCache.TryGetValue(cacheKey, out object? value))
{
return Must.NotNull((Dictionary<TKey, TValue>)value!);
}
List<TData> list = await FromCacheOrFileAsync<List<TData>>(fileName, token).ConfigureAwait(false);
Dictionary<TKey, TValue> dict = list.ToDictionaryOverride(keySelector, valueSelector);
return memoryCache.Set(cacheKey, dict);
}
}

View File

@@ -1,12 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Automation.Provider;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户服务
@@ -16,45 +16,28 @@ public interface IUserService
/// <summary>
/// 获取或设置当前用户
/// </summary>
User? CurrentUser { get; set; }
BindingUser? Current { get; set; }
/// <summary>
/// 初始化用户服务及所有用户
/// 异步获取同步的用户信息集合
/// 对集合的操作应通过服务抽象完成
/// 此操作不能取消
/// </summary>
/// <returns>准备完成的用户信息枚举</returns>
Task<ObservableCollection<User>> GetUserCollectionAsync();
/// <returns>准备完成的用户信息集合</returns>
Task<ObservableCollection<BindingUser>> GetUserCollectionAsync();
/// <summary>
/// 异步添加用户
/// 通常用户是未初始化的
/// 尝试异步处理输入的Cookie
/// </summary>
/// <param name="user">待添加的用户</param>
/// <param name="uid">用户的米游社UID,用于检查是否包含重复的用户</param>
/// <returns>用户初始化是否成功</returns>
Task<UserAddResult> TryAddUserAsync(User user, string uid);
/// <summary>
/// 尝试使用 login_ticket 升级用户
/// </summary>
/// <param name="cookie">额外的Cookie</param>
/// <param name="token">取消令牌</param>
/// <returns>是否升级成功</returns>
Task<ValueResult<bool, string>> TryUpgradeUserAsync(IDictionary<string, string> addiition, CancellationToken token = default);
/// <param name="cookie">Cookie</param>
/// <returns>处理的结果</returns>
Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie);
/// <summary>
/// 异步移除用户
/// </summary>
/// <param name="user">待移除的用户</param>
/// <returns>任务</returns>
Task RemoveUserAsync(User user);
/// <summary>
/// 创建一个新的绑定用户
/// 若存在 login_ticket 与 login_uid 则 自动获取 stoken
/// </summary>
/// <param name="cookie">cookie的字符串形式</param>
/// <returns>新的绑定用户</returns>
Task<User?> CreateUserAsync(IDictionary<string, string> cookie);
Task RemoveUserAsync(BindingUser user);
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Threading;
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 System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户帮助类
/// </summary>
internal static class UserHelper
{
/// <summary>
/// 尝试获取用户
/// </summary>
/// <param name="users">待查找的用户集合</param>
/// <param name="uid">uid</param>
/// <param name="user">用户</param>
/// <returns>是否存在用户</returns>
public static bool TryGetUserByUid(ObservableCollection<BindingUser> users, string uid, [NotNullWhen(true)] out BindingUser? user)
{
user = users.SingleOrDefault(u => u.UserInfo!.Uid == uid);
return user != null;
}
}

View File

@@ -1,25 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户添加操作结果
/// </summary>
public enum UserAddResult
public enum UserOptionResult
{
/// <summary>
/// 添加成功
/// </summary>
Added,
/// <summary>
/// Cookie不完整
/// </summary>
Incomplete,
/// <summary>
/// Cookie信息已经失效
/// </summary>
Invalid,
/// <summary>
/// 用户的Cookie成功更新
/// </summary>
Updated,
/// <summary>
/// 已经存在该用户
/// 升级到Stoken
/// </summary>
AlreadyExists,
Upgraded,
}

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
@@ -12,7 +11,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Service;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户服务
@@ -23,7 +22,7 @@ internal class UserService : IUserService
{
private readonly AppDbContext appDbContext;
private readonly UserClient userClient;
private readonly BindingClient userGameRoleClient;
private readonly BindingClient bindingClient;
private readonly AuthClient authClient;
private readonly IMessenger messenger;
@@ -35,25 +34,25 @@ internal class UserService : IUserService
/// </summary>
/// <param name="appDbContext">应用程序数据库上下文</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="bindingClient">角色客户端</param>
/// <param name="authClient">验证客户端</param>
/// <param name="messenger">消息器</param>
public UserService(
AppDbContext appDbContext,
UserClient userClient,
BindingClient userGameRoleClient,
BindingClient bindingClient,
AuthClient authClient,
IMessenger messenger)
{
this.appDbContext = appDbContext;
this.userClient = userClient;
this.userGameRoleClient = userGameRoleClient;
this.bindingClient = bindingClient;
this.authClient = authClient;
this.messenger = messenger;
}
/// <inheritdoc/>
public BindingUser? CurrentUser
public BindingUser? Current
{
get => currentUser;
set
@@ -90,44 +89,6 @@ internal class UserService : IUserService
}
}
/// <inheritdoc/>
public async Task<UserAddResult> TryAddUserAsync(BindingUser newUser, string uid)
{
Must.NotNull(userCollection!);
// 查找是否有相同的uid
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
{
// Prevent users from adding a completely same cookie.
if (userWithSameUid.Cookie == newUser.Cookie)
{
return UserAddResult.AlreadyExists;
}
else
{
// Update user cookie here.
userWithSameUid.Cookie = newUser.Cookie;
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return UserAddResult.Updated;
}
}
else
{
Verify.Operation(newUser.IsInitialized, "该用户尚未初始化");
// Sync cache
userCollection.Add(newUser);
// Sync database
appDbContext.Users.Add(newUser.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return UserAddResult.Added;
}
}
/// <inheritdoc/>
public Task RemoveUserAsync(BindingUser user)
{
@@ -151,7 +112,7 @@ internal class UserService : IUserService
foreach (Model.Entity.User entity in appDbContext.Users)
{
BindingUser? initialized = await BindingUser
.ResumeAsync(entity, userClient, userGameRoleClient)
.ResumeAsync(entity, userClient, bindingClient)
.ConfigureAwait(false);
if (initialized != null)
@@ -167,37 +128,94 @@ internal class UserService : IUserService
}
userCollection = new(users);
CurrentUser = users.SingleOrDefault(user => user.IsSelected);
Current = users.SingleOrDefault(user => user.IsSelected);
}
return userCollection;
}
/// <inheritdoc/>
public Task<BindingUser?> CreateUserAsync(IDictionary<string, string> cookie)
{
return BindingUser.CreateAsync(cookie, userClient, userGameRoleClient, authClient);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> TryUpgradeUserAsync(IDictionary<string, string> addition, CancellationToken token = default)
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
{
Must.NotNull(userCollection!);
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? uid))
// 检查 uid 是否存在
if (cookie.TryGetUid(out string? uid))
{
// 查找是否有相同的uid
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
// 检查 login ticket 是否存在
// 若存在则尝试升级至 stoken
await TryAddMultiTokenAsync(cookie, uid).ConfigureAwait(false);
// 检查 uid 对应用户是否存在
if (UserHelper.TryGetUserByUid(userCollection, uid, out BindingUser? userWithSameUid))
{
// Update user cookie here.
if (await userWithSameUid.TryUpgradeAsync(addition, authClient, token))
// 检查 stoken 是否存在
if (cookie.ContainsSToken())
{
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(true, uid);
// insert stoken directly
userWithSameUid.Cookie.InsertSToken(uid, cookie);
return new(UserOptionResult.Upgraded, uid);
}
if (cookie.ContainsLTokenAndCookieToken())
{
UpdateUserCookie(cookie, userWithSameUid);
return new(UserOptionResult.Updated, uid);
}
}
else if (cookie.ContainsLTokenAndCookieToken())
{
return await TryCreateUserAndAddAsync(userCollection, cookie).ConfigureAwait(false);
}
}
return new(false, string.Empty);
return new(UserOptionResult.Incomplete, null!);
}
private async Task TryAddMultiTokenAsync(Cookie cookie, string uid)
{
if (cookie.TryGetLoginTicket(out string? loginTicket))
{
// get multitoken
Dictionary<string, string> multiToken = await authClient
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
.ConfigureAwait(false);
if (multiToken.Count >= 2)
{
cookie.InsertMultiToken(uid, multiToken);
cookie.RemoveLoginTicket();
}
}
}
private void UpdateUserCookie(Cookie cookie, BindingUser user)
{
user.Cookie = cookie;
appDbContext.Users.Update(user.Entity);
appDbContext.SaveChanges();
}
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
{
cookie.Trim();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
if (newUser != null)
{
// Sync cache
await ThreadHelper.SwitchToMainThreadAsync();
users.Add(newUser);
// Sync database
appDbContext.Users.Add(newUser.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
}
else
{
return new(UserOptionResult.Invalid, null!);
}
}
}

View File

@@ -21,7 +21,7 @@
<PackageCertificateThumbprint>F8C2255969BEA4A681CED102771BF807856AEC02</PackageCertificateThumbprint>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
<AppxSymbolPackageEnabled>True</AppxSymbolPackageEnabled>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Never</AppxBundle>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
@@ -31,6 +31,7 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Control\Panel\PanelSelector.xaml" />
<None Remove="NativeMethods.json" />
<None Remove="NativeMethods.txt" />
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
@@ -38,6 +39,8 @@
<None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<None Remove="Resource\Icon\UI_BtnIcon_Gacha.png" />
<None Remove="Resource\Icon\UI_Icon_Achievement.png" />
<None Remove="Resource\Icon\UI_Icon_BoostUp.png" />
<None Remove="Resource\Icon\UI_Icon_Locked.png" />
<None Remove="Resource\Icon\UI_Icon_None.png" />
<None Remove="Resource\Icon\UI_ItemIcon_201.png" />
<None Remove="Resource\Segoe Fluent Icons.ttf" />
@@ -48,6 +51,7 @@
<None Remove="View\Control\StatisticsCard.xaml" />
<None Remove="View\Dialog\AchievementArchiveCreateDialog.xaml" />
<None Remove="View\Dialog\AchievementImportDialog.xaml" />
<None Remove="View\Dialog\AvatarInfoQueryDialog.xaml" />
<None Remove="View\Dialog\GachaLogImportDialog.xaml" />
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="View\Dialog\UserAutoCookieDialog.xaml" />
@@ -56,6 +60,7 @@
<None Remove="View\Page\AchievementPage.xaml" />
<None Remove="View\Page\AnnouncementContentPage.xaml" />
<None Remove="View\Page\AnnouncementPage.xaml" />
<None Remove="View\Page\AvatarPropertyPage.xaml" />
<None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
@@ -83,6 +88,8 @@
<Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<Content Include="Resource\Icon\UI_BtnIcon_Gacha.png" />
<Content Include="Resource\Icon\UI_Icon_Achievement.png" />
<Content Include="Resource\Icon\UI_Icon_BoostUp.png" />
<Content Include="Resource\Icon\UI_Icon_Locked.png" />
<Content Include="Resource\Icon\UI_Icon_None.png" />
<Content Include="Resource\Icon\UI_ItemIcon_201.png" />
</ItemGroup>
@@ -92,6 +99,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<!-- Prevent NewtownSoft.Json -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps files-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9" />
@@ -103,9 +111,12 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.64" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.63-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25211-preview" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.220930.4-preview2" />
<PackageReference Include="MiniExcel" Version="1.28.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
@@ -128,6 +139,16 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\AvatarInfoQueryDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\AvatarPropertyPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\UserAutoCookieDialog.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -233,4 +254,9 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Panel\PanelSelector.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -4,9 +4,12 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:cwuconv="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shmbg="using:Snap.Hutao.Model.Binding.Gacha"
xmlns:shvc="using:Snap.Hutao.View.Converter"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shmbg:TypedWishSummary}">
@@ -15,8 +18,8 @@
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<DataTemplate x:Key="OrangeListTemplate" x:DataType="shmbg:SummaryItem">
<Grid Margin="0,4,4,0" Background="Transparent" >
<ToolTipService.ToolTip>
@@ -60,8 +63,54 @@
Style="{StaticResource BodyTextBlockStyle}"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem">
<Grid Width="40" Margin="0,4,4,0">
<ToolTipService.ToolTip>
<TextBlock Text="{Binding TimeFormatted}"/>
</ToolTipService.ToolTip>
<StackPanel>
<shci:CachedImage
Source="{Binding Icon}"
Height="40" Width="40"/>
<TextBlock
Text="{Binding LastPull}"
Style="{StaticResource CaptionTextBlockStyle}"
HorizontalAlignment="Center"
TextWrapping="NoWrap">
<TextBlock.Foreground>
<SolidColorBrush Color="{Binding Color}"/>
</TextBlock.Foreground>
</TextBlock>
</StackPanel>
<!--<StackPanel
HorizontalAlignment="Right"
Orientation="Horizontal">
<TextBlock
Margin="0,0,8,0"
Foreground="#FF0063FF"
Text="保底"
VerticalAlignment="Center"
Visibility="{Binding IsGuarentee,Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Margin="0,0,8,0"
Text="UP"
Foreground="#FFFFA400"
VerticalAlignment="Center"
Visibility="{Binding IsUp,Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Width="20"
TextAlignment="Center"
Text="{Binding LastPull}"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"/>
</StackPanel>-->
</Grid>
</DataTemplate>
</UserControl.Resources>
<Border
@@ -74,6 +123,7 @@
<RowDefinition/>
</Grid.RowDefinitions>
<Expander
x:Name="DetailExpander"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="Transparent"
@@ -86,6 +136,17 @@
VerticalAlignment="Center"
Text="{Binding Name}"
Style="{StaticResource SubtitleTextBlockStyle}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock
Margin="0,4,12,4"
FontFamily="Consolas"
Text="{Binding TotalCount}"
Visibility="{Binding ElementName=DetailExpander,Path=IsExpanded,Converter={StaticResource BoolToVisibilityRevertConverter}}"
FontSize="24"/>
<shcp:PanelSelector
Margin="6,0,6,0"
x:Name="ItemsPanelSelector"/>
</StackPanel>
</Grid>
</Expander.Header>
<StackPanel>
@@ -232,9 +293,32 @@
Grid.Row="2"
Margin="12,6,12,12"
VerticalScrollBarVisibility="Hidden">
<ItemsControl
ItemsSource="{Binding OrangeList}"
ItemTemplate="{StaticResource OrangeListTemplate}"/>
<cwucont:SwitchPresenter Value="{Binding ElementName=ItemsPanelSelector,Path=Current}">
<cwucont:SwitchPresenter.ContentTransitions>
<ContentThemeTransition/>
</cwucont:SwitchPresenter.ContentTransitions>
<cwucont:Case Value="List">
<ItemsControl
ItemsSource="{Binding OrangeList}"
ItemTemplate="{StaticResource OrangeListTemplate}"/>
</cwucont:Case>
<cwucont:Case Value="Grid">
<ItemsControl
Margin="0,0,-4,0"
ItemsSource="{Binding OrangeList}"
ItemTemplate="{StaticResource OrangeGridTemplate}">
<ItemsControl.Transitions>
<ReorderThemeTransition/>
</ItemsControl.Transitions>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwucont:WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</cwucont:Case>
</cwucont:SwitchPresenter>
</ScrollViewer>
</Grid>
</Border>

View File

@@ -1,9 +1,8 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.AchievementArchiveCreateDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Dialog"
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"
mc:Ignorable="d"
Title="设置成就存档的名称"

View File

@@ -5,8 +5,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
mc:Ignorable="d"
Title="为当前存档导入成就"
DefaultButton="Primary"
@@ -14,10 +12,6 @@
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<mxi:Interaction.Behaviors>
<shcb:ContentDialogBehavior/>
</mxi:Interaction.Behaviors>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>

View File

@@ -44,8 +44,6 @@ public sealed partial class AchievementImportDialog : ContentDialog
/// <returns>导入选项</returns>
public async Task<ValueResult<bool, ImportStrategy>> GetImportStrategyAsync()
{
//await ThreadHelper.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex;

Some files were not shown because too many files have changed in this diff Show More