Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
3dea892ed7 impl #1355 2024-04-16 12:38:37 +08:00
98 changed files with 1043 additions and 1089 deletions

View File

@@ -28,21 +28,4 @@ internal static class FrameworkElementExtension
frameworkElement.IsRightTapEnabled = false;
frameworkElement.IsTabStop = false;
}
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
where TDataContext : class
{
IServiceProvider service = serviceProvider ?? Ioc.Default;
try
{
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
}
catch (Exception ex)
{
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
logger?.LogError(ex, "Failed to initialize DataContext");
throw;
}
}
}

View File

@@ -35,7 +35,7 @@ internal sealed class CachedImage : Implementation.ImageEx
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), HutaoExceptionKind.ImageCacheInvalidUri, SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
@@ -19,14 +18,13 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size MeasureOverride(Size availableSize)
{
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
foreach (UIElement child in Children)
{
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count;
double childAvailableWidth = (availableWidth + Spacing) / Children.Count;
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
}
return base.MeasureOverride(availableSize);
@@ -34,14 +32,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size ArrangeOverride(Size finalSize)
{
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count;
int itemCount = Children.Count;
double availableItemWidth = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
double offset = 0;
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
foreach (UIElement child in Children)
{
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
child.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
offset += actualItemWidth + Spacing;
}
@@ -51,8 +49,7 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
private static void OnLoaded(object sender, RoutedEventArgs e)
{
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
panel.MinWidth = (panel.MinItemWidth * panel.Children.Count) + (panel.Spacing * (panel.Children.Count - 1));
}
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)

View File

@@ -15,7 +15,7 @@ internal class ScopedPage : Page
{
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope pageScope;
private readonly IServiceScope currentScope;
private bool inFrame = true;
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
public async ValueTask NotifyRecipientAsync(INavigationData extra)
@@ -44,17 +44,9 @@ internal class ScopedPage : Page
protected void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
try
{
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
/// <inheritdoc/>
@@ -103,7 +95,7 @@ internal class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
pageScope.Dispose();
currentScope.Dispose();
}
}
}

View File

@@ -5,43 +5,52 @@ namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(string message, Exception? innerException)
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
private HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
[DoesNotReturn]
public static HutaoException Throw(string message, Exception? innerException = default)
public static HutaoException Throw(HutaoExceptionKind kind, string message, Exception? innerException = default)
{
throw new HutaoException(message, innerException);
throw new HutaoException(kind, message, innerException);
}
public static void ThrowIf(bool condition, string message, Exception? innerException = default)
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
{
if (condition)
{
throw new HutaoException(message, innerException);
throw new HutaoException(kind, message, innerException);
}
}
public static void ThrowIfNot(bool condition, string message, Exception? innerException = default)
public static void ThrowIfNot(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
{
if (!condition)
{
throw new HutaoException(message, innerException);
throw new HutaoException(kind, message, innerException);
}
}
[DoesNotReturn]
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default)
{
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
string message = SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id);
throw new HutaoException(HutaoExceptionKind.GachaStatisticsInvalidItemId, message, innerException);
}
[DoesNotReturn]
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
{
throw new HutaoException(message, innerException);
throw new HutaoException(HutaoExceptionKind.UserdataCorrupted, message, innerException);
}
[DoesNotReturn]
@@ -51,12 +60,6 @@ internal sealed class HutaoException : Exception
throw new InvalidCastException(message, innerException);
}
[DoesNotReturn]
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
{
throw new NotSupportedException(message, innerException);
}
[DoesNotReturn]
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
{

View File

@@ -29,7 +29,7 @@ internal readonly struct TempFile : IDisposable
}
catch (UnauthorizedAccessException ex)
{
HutaoException.Throw(SH.CoreIOTempFileCreateFail, ex);
HutaoException.Throw(HutaoExceptionKind.FileSystemCreateFileInsufficientPermissions, SH.CoreIOTempFileCreateFail, ex);
}
if (delete)

View File

@@ -49,7 +49,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
{
byte[] content = new byte[header->ContentLength];
serverStream.ReadAtLeast(content, header->ContentLength, false);
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, HutaoExceptionKind.PrivateNamedPipeContentHashIncorrect, "PipePacket Content Hash incorrect");
return content;
}

View File

@@ -118,6 +118,14 @@ internal static partial class EnumerableExtension
collection.RemoveAt(collection.Count - 1);
}
/// <summary>
/// 转换到新类型的列表
/// </summary>
/// <typeparam name="TSource">原始类型</typeparam>
/// <typeparam name="TResult">新类型</typeparam>
/// <param name="list">列表</param>
/// <param name="selector">选择器</param>
/// <returns>新类型的列表</returns>
[Pure]
public static List<TResult> SelectList<TSource, TResult>(this List<TSource> list, Func<TSource, TResult> selector)
{

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.ViewModel.Game;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
@@ -36,7 +35,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
scope = serviceProvider.CreateScope();
windowOptions = new(this, DragableGrid, new(MaxWidth, MaxHeight));
this.InitializeController(serviceProvider);
RootGrid.InitializeDataContext<LaunchGameViewModel>(scope.ServiceProvider);
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
}
/// <inheritdoc/>

View File

@@ -5,5 +5,5 @@ namespace Snap.Hutao.Model.Entity.Abstraction;
internal interface IAppDbEntity
{
Guid InnerId { get; }
Guid InnerId { get; set; }
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Entity.Abstraction;
internal interface IAppDbEntityHasArchive : IAppDbEntity
{
Guid ArchiveId { get; }
}

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Model.Entity;
[HighQuality]
[Table("achievements")]
[SuppressMessage("", "SA1124")]
internal sealed class Achievement : IAppDbEntityHasArchive,
internal sealed class Achievement : IAppDbEntity,
IEquatable<Achievement>,
IDbMappingForeignKeyFrom<Achievement, AchievementId>,
IDbMappingForeignKeyFrom<Achievement, UIAFItem>

View File

@@ -19,7 +19,7 @@ internal sealed partial class SettingEntry
public const string AnnouncementRegion = "AnnouncementRegion";
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
public const string IsUnobtainedWishItemVisible = "IsUnobtainedWishItemVisible";
public const string IsNeverHeldStatisticsItemVisible = "IsNeverHeldStatisticsItemVisible";
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";

View File

@@ -11,6 +11,11 @@ namespace Snap.Hutao.Model.Metadata.Converter;
[HighQuality]
internal sealed class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?, Uri>
{
/// <summary>
/// 从角色转换到名片
/// </summary>
/// <param name="avatar">角色</param>
/// <returns>名片</returns>
public static Uri AvatarToUri(Avatar.Avatar? avatar)
{
if (avatar is null)

View File

@@ -2636,6 +2636,12 @@
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingNeverHeldItemVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面显示或隐藏未持有过的角色或武器</value>
</data>
<data name="ViewPageSettingNeverHeldItemVisibleHeader" xml:space="preserve">
<value>未持有过的角色或武器</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官网</value>
</data>
@@ -2702,12 +2708,6 @@
<data name="ViewPageSettingTranslateNavigate" xml:space="preserve">
<value>贡献翻译</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面角色与武器页签显示未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
<value>未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUpdateCheckAction" xml:space="preserve">
<value>前往商店</value>
</data>

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Abstraction;
@@ -20,16 +19,4 @@ internal static class AppDbServiceAppDbEntityExtension
{
return service.ExecuteAsync((dbset, token) => dbset.ExecuteDeleteWhereAsync(e => e.InnerId == entity.InnerId, token), token);
}
public static List<TEntity> ListByArchiveId<TEntity>(this IAppDbService<TEntity> service, Guid archiveId)
where TEntity : class, IAppDbEntityHasArchive
{
return service.Query(query => query.Where(e => e.ArchiveId == archiveId).ToList());
}
public static ValueTask<List<TEntity>> ListByArchiveIdAsync<TEntity>(this IAppDbService<TEntity> service, Guid archiveId, CancellationToken token = default)
where TEntity : class, IAppDbEntityHasArchive
{
return service.QueryAsync((query, token) => query.Where(e => e.ArchiveId == archiveId).ToListAsync(token), token);
}
}

View File

@@ -1,47 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
namespace Snap.Hutao.Service.Abstraction;
internal static class AppDbServiceCollectionExtension
{
public static List<TEntity> List<TEntity>(this IAppDbService<TEntity> service)
where TEntity : class
{
return service.Query(query => query.ToList());
}
public static List<TEntity> List<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.Where(predicate).ToList());
}
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.ToListAsync(token), token);
}
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.Where(predicate).ToListAsync(token), token);
}
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service)
where TEntity : class
{
return service.Query(query => query.ToObservableCollection());
}
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.Where(predicate).ToObservableCollection());
}
}

View File

@@ -84,6 +84,18 @@ internal static class AppDbServiceExtension
return service.ExecuteAsync((dbset, token) => dbset.AddRangeAndSaveAsync(entities, token), token);
}
public static TEntity Single<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Execute(dbset => dbset.AsNoTracking().Single(predicate));
}
public static ValueTask<TEntity> SingleAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.AsNoTracking().SingleAsync(predicate, token), token);
}
public static TResult Query<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, TResult> func)
where TEntity : class
{
@@ -114,18 +126,6 @@ internal static class AppDbServiceExtension
return service.ExecuteAsync((dbset, token) => func(dbset.AsNoTracking(), token), token);
}
public static TEntity Single<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.Single(predicate));
}
public static ValueTask<TEntity> SingleAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.SingleAsync(predicate, token), token);
}
public static int Update<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{

View File

@@ -11,6 +11,9 @@ using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 成就数据库服务
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IAchievementDbService))]
internal sealed partial class AchievementDbService : IAchievementDbService
@@ -76,7 +79,7 @@ internal sealed partial class AchievementDbService : IAchievementDbService
public ObservableCollection<AchievementArchive> GetAchievementArchiveCollection()
{
return this.ObservableCollection<AchievementArchive>();
return this.Query<AchievementArchive, ObservableCollection<AchievementArchive>>(query => query.ToObservableCollection());
}
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive, CancellationToken token = default)
@@ -87,21 +90,25 @@ internal sealed partial class AchievementDbService : IAchievementDbService
public List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId)
{
return this.ListByArchiveId<EntityAchievement>(archiveId);
return this.Query<EntityAchievement, List<EntityAchievement>>(query => [.. query.Where(a => a.ArchiveId == archiveId)]);
}
public ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
{
return this.ListByArchiveIdAsync<EntityAchievement>(archiveId, token);
return this.QueryAsync<EntityAchievement, List<EntityAchievement>>(
(query, token) => query
.Where(a => a.ArchiveId == archiveId)
.ToListAsync(token),
token);
}
public List<AchievementArchive> GetAchievementArchiveList()
{
return this.List<AchievementArchive>();
return this.Query<AchievementArchive, List<AchievementArchive>>(query => [.. query]);
}
public ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default)
public async ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default)
{
return this.ListAsync<AchievementArchive>(token);
return await this.QueryAsync<AchievementArchive, List<AchievementArchive>>(query => query.ToListAsync()).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 集合部分
/// </summary>
internal sealed partial class AchievementService
{
private ObservableCollection<AchievementArchive>? archiveCollection;
/// <inheritdoc/>
public AchievementArchive? CurrentArchive
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
}
/// <inheritdoc/>
public ObservableCollection<AchievementArchive> ArchiveCollection
{
get
{
if (archiveCollection is null)
{
archiveCollection = achievementDbService.GetAchievementArchiveCollection();
CurrentArchive = archiveCollection.SelectedOrDefault();
}
return archiveCollection;
}
}
/// <inheritdoc/>
public async ValueTask<ArchiveAddResult> AddArchiveAsync(AchievementArchive newArchive)
{
if (string.IsNullOrWhiteSpace(newArchive.Name))
{
return ArchiveAddResult.InvalidName;
}
ArgumentNullException.ThrowIfNull(archiveCollection);
// 查找是否有相同的名称
if (archiveCollection.Any(a => a.Name == newArchive.Name))
{
return ArchiveAddResult.AlreadyExists;
}
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Add(newArchive);
// Sync database
await taskContext.SwitchToBackgroundAsync();
CurrentArchive = newArchive;
return ArchiveAddResult.Added;
}
/// <inheritdoc/>
public async ValueTask RemoveArchiveAsync(AchievementArchive archive)
{
ArgumentNullException.ThrowIfNull(archiveCollection);
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Remove(archive);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await achievementDbService.RemoveAchievementArchiveAsync(archive).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.Achievement;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 数据交换部分
/// </summary>
internal sealed partial class AchievementService
{
/// <inheritdoc/>
public async ValueTask<ImportResult> ImportFromUIAFAsync(AchievementArchive archive, List<UIAFItem> list, ImportStrategy strategy)
{
await taskContext.SwitchToBackgroundAsync();
Guid archiveId = archive.InnerId;
switch (strategy)
{
case ImportStrategy.AggressiveMerge:
{
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, true);
}
case ImportStrategy.LazyMerge:
{
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, false);
}
case ImportStrategy.Overwrite:
{
IEnumerable<EntityAchievement> orederedUIAF = list
.Select(uiaf => EntityAchievement.From(archiveId, uiaf))
.OrderBy(a => a.Id);
return achievementDbBulkOperation.Overwrite(archiveId, orederedUIAF);
}
default:
throw Must.NeverHappen();
}
}
/// <inheritdoc/>
public async ValueTask<UIAF> ExportToUIAFAsync(AchievementArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
List<EntityAchievement> entities = await achievementDbService
.GetAchievementListByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
List<UIAFItem> list = entities.SelectList(UIAFItem.From);
return new()
{
Info = UIAFInfo.From(runtimeOptions),
List = list,
};
}
}

View File

@@ -3,16 +3,17 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Achievement;
using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement;
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 成就服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
@@ -24,127 +25,21 @@ internal sealed partial class AchievementService : IAchievementService
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private ObservableCollection<AchievementArchive>? archiveCollection;
public AchievementArchive? CurrentArchive
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
}
public ObservableCollection<AchievementArchive> ArchiveCollection
{
get
{
if (archiveCollection is null)
{
archiveCollection = achievementDbService.GetAchievementArchiveCollection();
CurrentArchive = archiveCollection.SelectedOrDefault();
}
return archiveCollection;
}
}
public List<AchievementView> GetAchievementViewList(AchievementArchive archive, AchievementServiceMetadataContext context)
/// <inheritdoc/>
public List<AchievementView> GetAchievementViewList(AchievementArchive archive, List<MetadataAchievement> metadata)
{
Dictionary<AchievementId, EntityAchievement> entities = achievementDbService.GetAchievementMapByArchiveId(archive.InnerId);
return context.Achievements.SelectList(meta =>
return metadata.SelectList(meta =>
{
EntityAchievement entity = entities.GetValueOrDefault(meta.Id) ?? EntityAchievement.From(archive.InnerId, meta.Id);
return new AchievementView(entity, meta);
});
}
/// <inheritdoc/>
public void SaveAchievement(AchievementView achievement)
{
achievementDbService.OverwriteAchievement(achievement.Entity);
}
public async ValueTask<ArchiveAddResultKind> AddArchiveAsync(AchievementArchive newArchive)
{
if (string.IsNullOrWhiteSpace(newArchive.Name))
{
return ArchiveAddResultKind.InvalidName;
}
ArgumentNullException.ThrowIfNull(archiveCollection);
if (archiveCollection.Any(a => a.Name == newArchive.Name))
{
return ArchiveAddResultKind.AlreadyExists;
}
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Add(newArchive);
// Sync database
await taskContext.SwitchToBackgroundAsync();
CurrentArchive = newArchive;
return ArchiveAddResultKind.Added;
}
public async ValueTask RemoveArchiveAsync(AchievementArchive archive)
{
ArgumentNullException.ThrowIfNull(archiveCollection);
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Remove(archive);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await achievementDbService.RemoveAchievementArchiveAsync(archive).ConfigureAwait(false);
}
public async ValueTask<ImportResult> ImportFromUIAFAsync(AchievementArchive archive, List<UIAFItem> list, ImportStrategyKind strategy)
{
await taskContext.SwitchToBackgroundAsync();
Guid archiveId = archive.InnerId;
switch (strategy)
{
case ImportStrategyKind.AggressiveMerge:
{
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, true);
}
case ImportStrategyKind.LazyMerge:
{
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, false);
}
case ImportStrategyKind.Overwrite:
{
IEnumerable<EntityAchievement> orederedUIAF = list
.SelectList(uiaf => EntityAchievement.From(archiveId, uiaf))
.SortBy(a => a.Id);
return achievementDbBulkOperation.Overwrite(archiveId, orederedUIAF);
}
default:
throw HutaoException.NotSupported();
}
}
public async ValueTask<UIAF> ExportToUIAFAsync(AchievementArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
List<EntityAchievement> entities = await achievementDbService
.GetAchievementListByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
List<UIAFItem> list = entities.SelectList(UIAFItem.From);
return new()
{
Info = UIAFInfo.From(runtimeOptions),
List = list,
};
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement;
namespace Snap.Hutao.Service.Achievement;
internal sealed class AchievementServiceMetadataContext : IMetadataContext,
IMetadataListAchievementSource,
IMetadataDictionaryIdAchievementSource
{
public List<MetadataAchievement> Achievements { get; set; } = default!;
public Dictionary<AchievementId, MetadataAchievement> IdAchievementMap { get; set; } = default!;
}

View File

@@ -13,34 +13,32 @@ namespace Snap.Hutao.Service.Achievement;
[Injection(InjectAs.Scoped, typeof(IAchievementStatisticsService))]
internal sealed partial class AchievementStatisticsService : IAchievementStatisticsService
{
private const int AchievementCardTakeCount = 2;
private readonly IAchievementDbService achievementDbService;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(AchievementServiceMetadataContext context, CancellationToken token = default)
public async ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(Dictionary<AchievementId, MetadataAchievement> achievementMap)
{
await taskContext.SwitchToBackgroundAsync();
List<AchievementStatistics> results = [];
foreach (AchievementArchive archive in await achievementDbService.GetAchievementArchiveListAsync(token).ConfigureAwait(false))
foreach (AchievementArchive archive in await achievementDbService.GetAchievementArchiveListAsync().ConfigureAwait(false))
{
int finishedCount = await achievementDbService
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId, token)
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
int totalCount = context.IdAchievementMap.Count;
int totalCount = achievementMap.Count;
List<EntityAchievement> achievements = await achievementDbService
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, AchievementCardTakeCount, token)
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, 2)
.ConfigureAwait(false);
results.Add(new()
{
DisplayName = archive.Name,
FinishDescription = AchievementStatistics.Format(finishedCount, totalCount, out _),
Achievements = achievements.SelectList(entity => new AchievementView(entity, context.IdAchievementMap[entity.Id])),
Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])),
});
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Achievement;
/// 存档添加结果
/// </summary>
[HighQuality]
internal enum ArchiveAddResultKind
internal enum ArchiveAddResult
{
/// <summary>
/// 添加成功

View File

@@ -32,7 +32,13 @@ internal interface IAchievementService
/// <returns>UIAF</returns>
ValueTask<UIAF> ExportToUIAFAsync(EntityArchive selectedArchive);
List<AchievementView> GetAchievementViewList(EntityArchive archive, AchievementServiceMetadataContext context);
/// <summary>
/// 获取整合的成就
/// </summary>
/// <param name="archive">用户</param>
/// <param name="metadata">元数据</param>
/// <returns>整合的成就</returns>
List<AchievementView> GetAchievementViewList(EntityArchive archive, List<MetadataAchievement> metadata);
/// <summary>
/// 异步导入UIAF数据
@@ -41,7 +47,7 @@ internal interface IAchievementService
/// <param name="list">UIAF数据</param>
/// <param name="strategy">选项</param>
/// <returns>导入结果</returns>
ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategyKind strategy);
ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy);
/// <summary>
/// 异步移除存档
@@ -61,5 +67,5 @@ internal interface IAchievementService
/// </summary>
/// <param name="newArchive">新存档</param>
/// <returns>存档添加结果</returns>
ValueTask<ArchiveAddResultKind> AddArchiveAsync(EntityArchive newArchive);
ValueTask<ArchiveAddResult> AddArchiveAsync(EntityArchive newArchive);
}

View File

@@ -9,5 +9,5 @@ namespace Snap.Hutao.Service.Achievement;
internal interface IAchievementStatisticsService
{
ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(AchievementServiceMetadataContext context, CancellationToken token = default);
ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(Dictionary<AchievementId, MetadataAchievement> achievementMap);
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Achievement;
/// 导入策略
/// </summary>
[HighQuality]
internal enum ImportStrategyKind
internal enum ImportStrategy
{
/// <summary>
/// 贪婪合并

View File

@@ -17,15 +17,16 @@ namespace Snap.Hutao.Service;
/// <inheritdoc/>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAnnouncementService))]
[Injection(InjectAs.Transient, typeof(IAnnouncementService))]
internal sealed partial class AnnouncementService : IAnnouncementService
{
private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly AnnouncementClient announcementClient;
private readonly ITaskContext taskContext;
private readonly IMemoryCache memoryCache;
/// <inheritdoc/>
public async ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default)
{
// 缓存中存在记录,直接返回
@@ -35,37 +36,29 @@ internal sealed partial class AnnouncementService : IAnnouncementService
}
await taskContext.SwitchToBackgroundAsync();
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
.GetAnnouncementsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
List<AnnouncementContent>? contents;
AnnouncementWrapper wrapper;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
if (!announcementWrapperResponse.IsOk())
{
AnnouncementClient announcementClient = scope.ServiceProvider.GetRequiredService<AnnouncementClient>();
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
.GetAnnouncementsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementWrapperResponse.IsOk())
{
return default!;
}
wrapper = announcementWrapperResponse.Data;
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
.GetAnnouncementContentsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementContentResponse.IsOk())
{
return default!;
}
contents = announcementContentResponse.Data.List;
return default!;
}
Dictionary<int, string> contentMap = contents.ToDictionary(id => id.AnnId, content => content.Content);
AnnouncementWrapper wrapper = announcementWrapperResponse.Data;
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
.GetAnnouncementContentsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementContentResponse.IsOk())
{
return default!;
}
List<AnnouncementContent> contents = announcementContentResponse.Data.List;
Dictionary<int, string> contentMap = contents
.ToDictionary(id => id.AnnId, content => content.Content);
// 将活动公告置于前方
wrapper.List.Reverse();
@@ -82,7 +75,8 @@ internal sealed partial class AnnouncementService : IAnnouncementService
{
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
item.Content = contentMap.GetValueOrDefault(item.AnnId, string.Empty);
contentMap.TryGetValue(item.AnnId, out string? rawContent);
item.Content = rawContent ?? string.Empty;
}
}

View File

@@ -15,8 +15,8 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private bool? isNeverHeldStatisticsItemVisible;
private bool? isEmptyHistoryWishVisible;
private bool? isUnobtainedWishItemVisible;
private BackdropType? backdropType;
private ElementTheme? elementTheme;
private BackgroundImageType? backgroundImageType;
@@ -25,14 +25,14 @@ internal sealed partial class AppOptions : DbStoreOptions
public bool IsEmptyHistoryWishVisible
{
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, false);
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
}
public bool IsUnobtainedWishItemVisible
public bool IsNeverHeldStatisticsItemVisible
{
get => GetOption(ref isUnobtainedWishItemVisible, SettingEntry.IsUnobtainedWishItemVisible, false);
set => SetOption(ref isUnobtainedWishItemVisible, SettingEntry.IsUnobtainedWishItemVisible, value);
get => GetOption(ref isNeverHeldStatisticsItemVisible, SettingEntry.IsNeverHeldStatisticsItemVisible);
set => SetOption(ref isNeverHeldStatisticsItemVisible, SettingEntry.IsNeverHeldStatisticsItemVisible, value);
}
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0);

View File

@@ -13,6 +13,9 @@ using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
@@ -20,11 +23,12 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
{
private readonly AvatarInfoDbBulkOperation avatarInfoDbBulkOperation;
private readonly IAvatarInfoDbService avatarInfoDbService;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ILogger<AvatarInfoService> logger;
private readonly IMetadataService metadataService;
private readonly ISummaryFactory summaryFactory;
private readonly EnkaClient enkaClient;
/// <inheritdoc/>
public async ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
@@ -88,13 +92,8 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
private async ValueTask<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
EnkaClient enkaClient = scope.ServiceProvider.GetRequiredService<EnkaClient>();
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
}
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
}
private async ValueTask<Summary> GetSummaryCoreAsync(IEnumerable<EntityAvatarInfo> avatarInfos, CancellationToken token)

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal sealed class AvatarViewBuilder : IAvatarViewBuilder
{
public ViewModel.AvatarProperty.AvatarView AvatarView { get; } = new();
}

View File

@@ -1,260 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class AvatarViewBuilderExtension
{
public static TBuilder ApplyCostumeIconOrDefault<TBuilder>(this TBuilder builder, Web.Enka.Model.AvatarInfo avatarInfo, Avatar avatar)
where TBuilder : IAvatarViewBuilder
{
if (avatarInfo.CostumeId.TryGetValue(out CostumeId id))
{
Costume costume = avatar.Costumes.Single(c => c.Id == id);
// Set to costume icon
builder.AvatarView.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
builder.AvatarView.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
}
else
{
builder.AvatarView.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.AvatarView.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
return builder;
}
public static TBuilder SetCalculatorRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
where TBuilder : IAvatarViewBuilder
{
return builder.SetCalculatorRefreshTimeFormat(refreshTime == DateTimeOffsetExtension.DatebaseDefaultTime ? defaultValue : format(refreshTime.ToLocalTime()));
}
public static TBuilder SetCalculatorRefreshTimeFormat<TBuilder>(this TBuilder builder, string calculatorRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.CalculatorRefreshTimeFormat = calculatorRefreshTimeFormat);
}
public static TBuilder SetConstellations<TBuilder>(this TBuilder builder, List<Skill> talents, List<SkillId>? talentIds)
where TBuilder : IAvatarViewBuilder
{
return builder.SetConstellations(CreateConstellations(talents, talentIds.EmptyIfNull()));
static List<ConstellationView> CreateConstellations(List<Skill> talents, List<SkillId> talentIds)
{
// TODO: use builder here
return talents.SelectList(talent => new ViewModel.AvatarProperty.ConstellationView()
{
Name = talent.Name,
Icon = SkillIconConverter.IconNameToUri(talent.Icon),
Description = talent.Description,
IsActivated = talentIds.Contains(talent.Id),
});
}
}
public static TBuilder SetConstellations<TBuilder>(this TBuilder builder, List<ConstellationView> constellations)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Constellations = constellations);
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, Dictionary<FightProperty, float>? fightPropMap)
where TBuilder : IAvatarViewBuilder
{
return builder.SetCritScore(ScoreCrit(fightPropMap));
static float ScoreCrit(Dictionary<FightProperty, float>? fightPropMap)
{
if (fightPropMap.IsNullOrEmpty())
{
return 0F;
}
float cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL];
float cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT];
return 100 * ((cr * 2) + cd);
}
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, float critScore)
where TBuilder : IAvatarViewBuilder
{
return builder.SetCritScore($"{critScore:F2}");
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, string critScore)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.CritScore = critScore);
}
public static TBuilder SetElement<TBuilder>(this TBuilder builder, ElementType element)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Element = element);
}
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, FetterLevel? level)
where TBuilder : IAvatarViewBuilder
{
if (level.TryGetValue(out FetterLevel value))
{
return builder.Configure(b => b.AvatarView.FetterLevel = value);
}
return builder;
}
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, uint level)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.FetterLevel = level);
}
public static TBuilder SetGameRecordRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
where TBuilder : IAvatarViewBuilder
{
return builder.SetGameRecordRefreshTimeFormat(refreshTime == DateTimeOffsetExtension.DatebaseDefaultTime ? defaultValue : format(refreshTime.ToLocalTime()));
}
public static TBuilder SetGameRecordRefreshTimeFormat<TBuilder>(this TBuilder builder, string gameRecordRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.GameRecordRefreshTimeFormat = gameRecordRefreshTimeFormat);
}
public static TBuilder SetId<TBuilder>(this TBuilder builder, AvatarId id)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Id = id);
}
public static TBuilder SetLevelNumber<TBuilder>(this TBuilder builder, uint? levelNumber)
where TBuilder : IAvatarViewBuilder
{
if (levelNumber.TryGetValue(out uint value))
{
return builder.Configure(b => b.AvatarView.LevelNumber = value);
}
return builder;
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Name = name);
}
public static TBuilder SetNameCard<TBuilder>(this TBuilder builder, Uri nameCard)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.NameCard = nameCard);
}
public static TBuilder SetProperties<TBuilder>(this TBuilder builder, List<AvatarProperty> properties)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Properties = properties);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Quality = quality);
}
public static TBuilder SetReliquaries<TBuilder>(this TBuilder builder, List<ReliquaryView> reliquaries)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Reliquaries = reliquaries);
}
public static TBuilder SetScore<TBuilder>(this TBuilder builder, float score)
where TBuilder : IAvatarViewBuilder
{
return builder.SetScore($"{score:F2}");
}
public static TBuilder SetScore<TBuilder>(this TBuilder builder, string score)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Score = score);
}
public static TBuilder SetShowcaseRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
where TBuilder : IAvatarViewBuilder
{
return builder.SetShowcaseRefreshTimeFormat(refreshTime == DateTimeOffsetExtension.DatebaseDefaultTime ? defaultValue : format(refreshTime.ToLocalTime()));
}
public static TBuilder SetShowcaseRefreshTimeFormat<TBuilder>(this TBuilder builder, string showcaseRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.ShowcaseRefreshTimeFormat = showcaseRefreshTimeFormat);
}
public static TBuilder SetSkills<TBuilder>(this TBuilder builder, Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
where TBuilder : IAvatarViewBuilder
{
return builder.SetSkills(CreateSkills(skillLevelMap, proudSkillExtraLevelMap, proudSkills));
static List<SkillView> CreateSkills(Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
{
if (skillLevelMap.IsNullOrEmpty())
{
return [];
}
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
if (proudSkillExtraLevelMap is not null)
{
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap)
{
skillExtraLeveledMap.IncreaseValue(proudSkills.Single(p => p.GroupId == groupId).Id, extraLevel);
}
}
return proudSkills.SelectList(proudableSkill =>
{
SkillId skillId = proudableSkill.Id;
// TODO: use builder here
return new SkillView()
{
Name = proudableSkill.Name,
Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
Description = proudableSkill.Description,
GroupId = proudableSkill.GroupId,
LevelNumber = skillLevelMap[skillId],
Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]),
};
});
}
}
public static TBuilder SetSkills<TBuilder>(this TBuilder builder, List<SkillView> skills)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Skills = skills);
}
public static TBuilder SetWeapon<TBuilder>(this TBuilder builder, WeaponView? weapon)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Weapon = weapon);
}
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IAvatarViewBuilder : IBuilder
{
ViewModel.AvatarProperty.AvatarView AvatarView { get; }
}

View File

@@ -5,8 +5,17 @@ using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述工厂
/// </summary>
[HighQuality]
internal interface ISummaryFactory
{
/// <summary>
/// 异步创建简述对象
/// </summary>
/// <param name="avatarInfos">角色列表</param>
/// <param name="token">取消令牌</param>
/// <returns>简述对象</returns>
ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token);
}

View File

@@ -5,9 +5,8 @@ using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Intrinsic.Format;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Service.AvatarInfo.Factory.Builder;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Enka.Model;
using System.Runtime.InteropServices;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
@@ -18,6 +17,9 @@ using PropertyWeapon = Snap.Hutao.ViewModel.AvatarProperty.WeaponView;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 单个角色工厂
/// </summary>
[HighQuality]
internal sealed class SummaryAvatarFactory
{
@@ -25,11 +27,16 @@ internal sealed class SummaryAvatarFactory
private readonly DateTimeOffset showcaseRefreshTime;
private readonly DateTimeOffset gameRecordRefreshTime;
private readonly DateTimeOffset calculatorRefreshTime;
private readonly SummaryFactoryMetadataContext context;
private readonly SummaryMetadataContext metadataContext;
public SummaryAvatarFactory(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo)
/// <summary>
/// 构造一个新的角色工厂
/// </summary>
/// <param name="metadataContext">元数据上下文</param>
/// <param name="avatarInfo">角色信息</param>
public SummaryAvatarFactory(SummaryMetadataContext metadataContext, EntityAvatarInfo avatarInfo)
{
this.context = context;
this.metadataContext = metadataContext;
this.avatarInfo = avatarInfo.Info;
showcaseRefreshTime = avatarInfo.ShowcaseRefreshTime;
@@ -37,51 +44,84 @@ internal sealed class SummaryAvatarFactory
calculatorRefreshTime = avatarInfo.CalculatorRefreshTime;
}
public static PropertyAvatar Create(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo)
{
return new SummaryAvatarFactory(context, avatarInfo).Create();
}
/// <summary>
/// 创建角色
/// </summary>
/// <returns>角色</returns>
public PropertyAvatar Create()
{
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = context.IdAvatarMap[avatarInfo.AvatarId];
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
PropertyAvatar propertyAvatar = new AvatarViewBuilder()
.SetId(avatar.Id)
.SetName(avatar.Name)
.SetQuality(avatar.Quality)
.SetNameCard(AvatarNameCardPicConverter.AvatarToUri(avatar))
.SetElement(ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore))
.SetConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList)
.SetSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents())
.SetFetterLevel(avatarInfo.FetterInfo?.ExpLevel)
.SetProperties(SummaryAvatarProperties.Create(avatarInfo.FightPropMap))
.SetCritScore(avatarInfo.FightPropMap)
.SetLevelNumber(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value)
.SetWeapon(reliquaryAndWeapon.Weapon)
.SetReliquaries(reliquaryAndWeapon.Reliquaries)
.SetScore(reliquaryAndWeapon.Reliquaries.Sum(r => r.Score))
.SetShowcaseRefreshTimeFormat(showcaseRefreshTime, SH.FormatServiceAvatarInfoSummaryShowcaseRefreshTimeFormat, SH.ServiceAvatarInfoSummaryShowcaseNotRefreshed)
.SetGameRecordRefreshTimeFormat(gameRecordRefreshTime, SH.FormatServiceAvatarInfoSummaryGameRecordRefreshTimeFormat, SH.ServiceAvatarInfoSummaryGameRecordNotRefreshed)
.SetCalculatorRefreshTimeFormat(calculatorRefreshTime, SH.FormatServiceAvatarInfoSummaryCalculatorRefreshTimeFormat, SH.ServiceAvatarInfoSummaryCalculatorNotRefreshed)
.ApplyCostumeIconOrDefault(avatarInfo, avatar)
.AvatarView;
PropertyAvatar propertyAvatar = new()
{
// metadata part
Id = avatar.Id,
Name = avatar.Name,
Quality = avatar.Quality,
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
// webinfo & metadata mixed part
Constellations = SummaryHelper.CreateConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList),
Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents()),
// webinfo part
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0U,
Properties = SummaryAvatarProperties.Create(avatarInfo.FightPropMap),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? 0U,
// processed webinfo part
Weapon = reliquaryAndWeapon.Weapon,
Reliquaries = reliquaryAndWeapon.Reliquaries,
Score = $"{reliquaryAndWeapon.Reliquaries.Sum(r => r.Score):F2}",
// times
ShowcaseRefreshTimeFormat = showcaseRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryShowcaseNotRefreshed
: SH.FormatServiceAvatarInfoSummaryShowcaseRefreshTimeFormat(showcaseRefreshTime.ToLocalTime()),
GameRecordRefreshTimeFormat = gameRecordRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryGameRecordNotRefreshed
: SH.FormatServiceAvatarInfoSummaryGameRecordRefreshTimeFormat(gameRecordRefreshTime.ToLocalTime()),
CalculatorRefreshTimeFormat = calculatorRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryCalculatorNotRefreshed
: SH.FormatServiceAvatarInfoSummaryCalculatorRefreshTimeFormat(calculatorRefreshTime.ToLocalTime()),
};
ApplyCostumeIconOrDefault(ref propertyAvatar, avatar);
return propertyAvatar;
}
private void ApplyCostumeIconOrDefault(ref PropertyAvatar propertyAvatar, MetadataAvatar avatar)
{
if (avatarInfo.CostumeId.TryGetValue(out CostumeId id))
{
Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == id);
// Set to costume icon
propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
propertyAvatar.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
}
else
{
propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
propertyAvatar.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
}
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
{
List<PropertyReliquary> reliquaryList = [];
PropertyWeapon? weapon = null;
foreach (ref readonly Equip equip in CollectionsMarshal.AsSpan(equipments))
foreach (Equip equip in equipments)
{
switch (equip.Flat.ItemType)
{
case ItemType.ITEM_RELIQUARY:
reliquaryList.Add(SummaryReliquaryFactory.Create(context, avatarInfo, equip));
SummaryReliquaryFactory summaryReliquaryFactory = new(metadataContext, avatarInfo, equip);
reliquaryList.Add(summaryReliquaryFactory.CreateReliquary());
break;
case ItemType.ITEM_WEAPON:
weapon = CreateWeapon(equip);
@@ -94,7 +134,7 @@ internal sealed class SummaryAvatarFactory
private PropertyWeapon CreateWeapon(Equip equip)
{
MetadataWeapon weapon = context.IdWeaponMap[equip.ItemId];
MetadataWeapon weapon = metadataContext.IdWeaponMap[equip.ItemId];
// AffixMap can be null when it's a white weapon.
ArgumentNullException.ThrowIfNull(equip.Weapon);
@@ -128,9 +168,7 @@ internal sealed class SummaryAvatarFactory
// EquipBase
Level = $"Lv.{equip.Weapon.Level.Value}",
Quality = weapon.Quality,
MainProperty = mainStat is not null
? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue)
: NameValueDefaults.String,
MainProperty = mainStat is not null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : NameValueDefaults.String,
// Weapon
Id = weapon.Id,

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -21,18 +20,23 @@ internal sealed partial class SummaryFactory : ISummaryFactory
/// <inheritdoc/>
public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token)
{
SummaryFactoryMetadataContext context = await metadataService
.GetContextAsync<SummaryFactoryMetadataContext>(token)
.ConfigureAwait(false);
SummaryMetadataContext metadataContext = new()
{
IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false),
IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false),
IdReliquaryAffixWeightMap = await metadataService.GetIdToReliquaryAffixWeightMapAsync(token).ConfigureAwait(false),
IdReliquaryMainAffixMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false),
IdReliquarySubAffixMap = await metadataService.GetIdToReliquarySubAffixMapAsync(token).ConfigureAwait(false),
ReliquaryLevels = await metadataService.GetReliquaryLevelListAsync(token).ConfigureAwait(false),
Reliquaries = await metadataService.GetReliquaryListAsync(token).ConfigureAwait(false),
};
IOrderedEnumerable<AvatarView> avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId))
.Select(a => SummaryAvatarFactory.Create(context, a))
.Select(a => new SummaryAvatarFactory(metadataContext, a).Create())
.OrderByDescending(a => a.LevelNumber)
.ThenByDescending(a => a.FetterLevel)
.ThenBy(a => a.Element);
.ThenBy(a => a.Name);
// TODO: thenby weapon type
return new()
{
Avatars = [.. avatars],

View File

@@ -1,7 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -11,6 +15,66 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
[HighQuality]
internal static class SummaryHelper
{
/// <summary>
/// 创建命之座
/// </summary>
/// <param name="talents">全部命座</param>
/// <param name="talentIds">激活的命座列表</param>
/// <returns>命之座</returns>
public static List<ConstellationView> CreateConstellations(List<Skill> talents, List<SkillId>? talentIds)
{
talentIds ??= [];
return talents.SelectList(talent => new ConstellationView()
{
Name = talent.Name,
Icon = SkillIconConverter.IconNameToUri(talent.Icon),
Description = talent.Description,
IsActivated = talentIds.Contains(talent.Id),
});
}
/// <summary>
/// 创建技能组
/// </summary>
/// <param name="skillLevelMap">技能等级映射</param>
/// <param name="proudSkillExtraLevelMap">额外提升等级映射</param>
/// <param name="proudSkills">技能列表</param>
/// <returns>技能</returns>
public static List<SkillView> CreateSkills(Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
{
if (skillLevelMap.IsNullOrEmpty())
{
return [];
}
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
if (proudSkillExtraLevelMap is not null)
{
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap)
{
skillExtraLeveledMap.IncreaseValue(proudSkills.Single(p => p.GroupId == groupId).Id, extraLevel);
}
}
return proudSkills.SelectList(proudableSkill =>
{
SkillId skillId = proudableSkill.Id;
return new SkillView()
{
Name = proudableSkill.Name,
Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
Description = proudableSkill.Description,
GroupId = proudableSkill.GroupId,
LevelNumber = skillLevelMap[skillId],
Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]),
};
});
}
/// <summary>
/// 获取副属性对应的最大属性的Id
/// </summary>
@@ -68,4 +132,22 @@ internal static class SummaryHelper
_ => throw Must.NeverHappen($"Unexpected AppendId: {appendId.Value} Delta: {delta}"),
};
}
/// <summary>
/// 获取双爆评分
/// </summary>
/// <param name="fightPropMap">属性</param>
/// <returns>评分</returns>
public static float ScoreCrit(Dictionary<FightProperty, float>? fightPropMap)
{
if (fightPropMap.IsNullOrEmpty())
{
return 0F;
}
float cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL];
float cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT];
return 100 * ((cr * 2) + cd);
}
}

View File

@@ -4,33 +4,51 @@
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述元数据上下文
/// 包含了所有制造简述需要的元数据
/// </summary>
[HighQuality]
internal sealed class SummaryFactoryMetadataContext : IMetadataContext,
IMetadataDictionaryIdAvatarSource,
IMetadataDictionaryIdWeaponSource,
IMetadataDictionaryIdReliquaryAffixWeightSource,
IMetadataDictionaryIdReliquaryMainPropertySource,
IMetadataDictionaryIdReliquarySubAffixSource,
IMetadataListReliquaryMainAffixLevelSource
internal sealed class SummaryMetadataContext
{
/// <summary>
/// 角色映射
/// </summary>
public Dictionary<AvatarId, MetadataAvatar> IdAvatarMap { get; set; } = default!;
/// <summary>
/// 武器映射
/// </summary>
public Dictionary<WeaponId, MetadataWeapon> IdWeaponMap { get; set; } = default!;
/// <summary>
/// 权重映射
/// </summary>
public Dictionary<AvatarId, ReliquaryAffixWeight> IdReliquaryAffixWeightMap { get; set; } = default!;
public Dictionary<ReliquaryMainAffixId, FightProperty> IdReliquaryMainPropertyMap { get; set; } = default!;
/// <summary>
/// 圣遗物主属性映射
/// </summary>
public Dictionary<ReliquaryMainAffixId, FightProperty> IdReliquaryMainAffixMap { get; set; } = default!;
/// <summary>
/// 圣遗物副属性映射
/// </summary>
public Dictionary<ReliquarySubAffixId, ReliquarySubAffix> IdReliquarySubAffixMap { get; set; } = default!;
public List<ReliquaryMainAffixLevel> ReliquaryMainAffixLevels { get; set; } = default!;
/// <summary>
/// 圣遗物等级
/// </summary>
public List<ReliquaryMainAffixLevel> ReliquaryLevels { get; set; } = default!;
/// <summary>
/// 圣遗物
/// </summary>
public List<MetadataReliquary> Reliquaries { get; set; } = default!;
}

View File

@@ -19,23 +19,28 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
[HighQuality]
internal sealed class SummaryReliquaryFactory
{
private readonly SummaryFactoryMetadataContext metadataContext;
private readonly SummaryMetadataContext metadataContext;
private readonly ModelAvatarInfo avatarInfo;
private readonly Web.Enka.Model.Equip equip;
public SummaryReliquaryFactory(SummaryFactoryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip)
/// <summary>
/// 构造一个新的圣遗物工厂
/// </summary>
/// <param name="metadataContext">元数据上下文</param>
/// <param name="avatarInfo">角色信息</param>
/// <param name="equip">圣遗物</param>
public SummaryReliquaryFactory(SummaryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip)
{
this.metadataContext = metadataContext;
this.avatarInfo = avatarInfo;
this.equip = equip;
}
public static ReliquaryView Create(SummaryFactoryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip)
{
return new SummaryReliquaryFactory(metadataContext, avatarInfo, equip).Create();
}
public ReliquaryView Create()
/// <summary>
/// 构造圣遗物
/// </summary>
/// <returns>圣遗物</returns>
public ReliquaryView CreateReliquary()
{
MetadataReliquary reliquary = metadataContext.Reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
@@ -64,8 +69,8 @@ internal sealed class SummaryReliquaryFactory
ArgumentNullException.ThrowIfNull(equip.Flat.ReliquarySubstats);
result.ComposedSubProperties = CreateComposedSubProperties(equip.Reliquary.AppendPropIdList);
ReliquaryMainAffixLevel relicLevel = metadataContext.ReliquaryMainAffixLevels.Single(r => r.Level == equip.Reliquary.Level && r.Rank == reliquary.RankLevel);
FightProperty property = metadataContext.IdReliquaryMainPropertyMap[equip.Reliquary.MainPropId];
ReliquaryMainAffixLevel relicLevel = metadataContext.ReliquaryLevels.Single(r => r.Level == equip.Reliquary.Level && r.Rank == reliquary.RankLevel);
FightProperty property = metadataContext.IdReliquaryMainAffixMap[equip.Reliquary.MainPropId];
result.MainProperty = FightPropertyFormat.ToNameValue(property, relicLevel.PropertyMap[property]);
result.Score = ScoreReliquary(property, reliquary, relicLevel, subProperty);
@@ -141,7 +146,7 @@ internal sealed class SummaryReliquaryFactory
// 从喵插件抓取的圣遗物评分权重
// 部分复杂的角色暂时使用了默认值
ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default);
ReliquaryMainAffixLevel? maxRelicLevel = metadataContext.ReliquaryMainAffixLevels.Where(r => r.Rank == reliquary.RankLevel).MaxBy(r => r.Level);
ReliquaryMainAffixLevel? maxRelicLevel = metadataContext.ReliquaryLevels.Where(r => r.Rank == reliquary.RankLevel).MaxBy(r => r.Level);
ArgumentNullException.ThrowIfNull(maxRelicLevel);
float percent = relicLevel.PropertyMap[property] / maxRelicLevel.PropertyMap[property];

View File

@@ -46,17 +46,11 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
{
DailyNoteEntry newEntry = DailyNoteEntry.From(userAndUid);
Web.Response.Response<WebDailyNote> dailyNoteResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IGameRecordClient gameRecordClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(roleUid));
dailyNoteResponse = await gameRecordClient
.GetDailyNoteAsync(userAndUid)
.ConfigureAwait(false);
}
Web.Response.Response<WebDailyNote> dailyNoteResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(roleUid))
.GetDailyNoteAsync(userAndUid)
.ConfigureAwait(false);
if (dailyNoteResponse.IsOk())
{
@@ -123,17 +117,11 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
continue;
}
Web.Response.Response<WebDailyNote> dailyNoteResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IGameRecordClient gameRecordClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(entry.Uid));
dailyNoteResponse = await gameRecordClient
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
}
Web.Response.Response<WebDailyNote> dailyNoteResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(entry.Uid))
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
if (dailyNoteResponse.IsOk())
{

View File

@@ -26,22 +26,15 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{
private static readonly FrozenSet<uint> BlueStandardWeaponIdsSet = FrozenSet.ToFrozenSet(
[
11301U, 11302U, 11306U,
12301U, 12302U, 12305U,
13303U,
14301U, 14302U, 14304U,
15301U, 15302U, 15304U
11301U, 11302U, 11306U, 12301U, 12302U, 12305U, 13303U, 14301U, 14302U, 14304U, 15301U, 15302U, 15304U
]);
private static readonly FrozenSet<uint> PurpleStandardWeaponIdsSet = FrozenSet.ToFrozenSet(
[
11401U, 11402U, 11403U, 11405U,
12401U, 12402U, 12403U, 12405U,
13401U, 13407U,
14401U, 14402U, 14403U, 14409U,
15401U, 15402U, 15403U, 15405U
11401U, 11402U, 11403U, 11405U, 12401U, 12402U, 12403U, 12405U, 13401U, 13407U, 14401U, 14402U, 14403U, 14409U, 15401U, 15402U, 15403U, 15405U
]);
private readonly IMetadataService metadataService;
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly ITaskContext taskContext;
private readonly AppOptions options;
@@ -52,7 +45,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
await taskContext.SwitchToBackgroundAsync();
List<HistoryWishBuilder> historyWishBuilders = context.GachaEvents.SelectList(gachaEvent => new HistoryWishBuilder(gachaEvent, context));
return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options);
return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible, options.IsNeverHeldStatisticsItemVisible);
}
private static GachaStatistics CreateCore(
@@ -61,7 +54,8 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
List<Model.Entity.GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders,
in GachaLogServiceMetadataContext context,
AppOptions appOptions)
bool isEmptyHistoryWishVisible,
bool isNeverHeldStatisticsItemVisible)
{
TypedWishSummaryBuilderContext standardContext = TypedWishSummaryBuilderContext.StandardWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder standardWishBuilder = new(standardContext);
@@ -81,7 +75,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
Dictionary<Weapon, int> purpleWeaponCounter = [];
Dictionary<Weapon, int> blueWeaponCounter = [];
if (appOptions.IsUnobtainedWishItemVisible)
if (isNeverHeldStatisticsItemVisible)
{
orangeAvatarCounter = context.IdAvatarMap.Values
.Where(avatar => avatar.Quality == QualityType.QUALITY_ORANGE)
@@ -206,7 +200,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{
// history
HistoryWishes = historyWishBuilders
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
.Where(b => isEmptyHistoryWishVisible || (!b.IsEmpty))
.OrderByDescending(builder => builder.From)
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
.Select(builder => builder.ToHistoryWish())

View File

@@ -19,7 +19,7 @@ internal sealed class HutaoStatisticsFactory
private readonly GachaEvent avatarEvent;
private readonly GachaEvent avatarEvent2;
private readonly GachaEvent weaponEvent;
private readonly GachaEvent? chronicledEvent;
private readonly GachaEvent chronicledEvent;
public HutaoStatisticsFactory(in HutaoStatisticsFactoryMetadataContext context)
{
@@ -32,7 +32,7 @@ internal sealed class HutaoStatisticsFactory
avatarEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.ActivityAvatar);
avatarEvent2 = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.SpecialActivityAvatar);
weaponEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.ActivityWeapon);
chronicledEvent = context.GachaEvents.SingleOrDefault(g => g.From < now && g.To > now && g.Type == GachaType.ActivityCity);
chronicledEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.ActivityCity);
}
public HutaoStatistics Create(GachaEventStatistics raw)
@@ -42,7 +42,7 @@ internal sealed class HutaoStatisticsFactory
AvatarEvent = CreateWishSummary(avatarEvent, raw.AvatarEvent),
AvatarEvent2 = CreateWishSummary(avatarEvent2, raw.AvatarEvent2),
WeaponEvent = CreateWishSummary(weaponEvent, raw.WeaponEvent),
Chronicled = chronicledEvent is null ? null : CreateWishSummary(chronicledEvent, raw.Chronicled),
Chronicled = CreateWishSummary(chronicledEvent, raw.Chronicled),
};
}

View File

@@ -19,12 +19,12 @@ internal static class GameFpsAddress
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext state, in RequiredGameModule requiredGameModule)
{
bool readOk = UnsafeReadModulesMemory(state.GameProcess, requiredGameModule, out VirtualMemory localMemory);
HutaoException.ThrowIfNot(readOk, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
HutaoException.ThrowIfNot(readOk, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
using (localMemory)
{
int offset = IndexOfPattern(localMemory.AsSpan()[(int)requiredGameModule.UnityPlayer.Size..]);
HutaoException.ThrowIfNot(offset >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
HutaoException.ThrowIfNot(offset >= 0, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerInterestedPatternNotFound);
byte* pLocalMemory = (byte*)localMemory.Pointer;
ref readonly Module unityPlayer = ref requiredGameModule.UnityPlayer;
@@ -76,7 +76,7 @@ internal static class GameFpsAddress
{
value = 0;
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _);
HutaoException.ThrowIfNot(result, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
HutaoException.ThrowIfNot(result, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
return result;
}
}

View File

@@ -30,10 +30,10 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
/// <inheritdoc/>
public async ValueTask<bool> UnlockAsync(CancellationToken token = default)
{
HutaoException.ThrowIfNot(context.IsUnlockerValid, "This Unlocker is invalid");
HutaoException.ThrowIfNot(context.IsUnlockerValid, HutaoExceptionKind.GameFpsUnlockingFailed, "This Unlocker is invalid");
(FindModuleResult result, RequiredGameModule gameModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false);
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound);
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleNoModuleFound);
GameFpsAddress.UnsafeFindFpsAddress(context, gameModule);
context.Report();

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdAchievementSource
{
public Dictionary<AchievementId, Model.Metadata.Achievement.Achievement> IdAchievementMap { get; set; }
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdReliquaryAffixWeightSource
{
public Dictionary<AvatarId, ReliquaryAffixWeight> IdReliquaryAffixWeightMap { get; set; }
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdReliquaryMainPropertySource
{
public Dictionary<ReliquaryMainAffixId, FightProperty> IdReliquaryMainPropertyMap { get; set; }
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdReliquarySubAffixSource
{
public Dictionary<ReliquarySubAffixId, ReliquarySubAffix> IdReliquarySubAffixMap { get; set; }
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataListAchievementSource
{
public List<Model.Metadata.Achievement.Achievement> Achievements { get; set; }
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Reliquary;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataListReliquaryMainAffixLevelSource
{
public List<ReliquaryMainAffixLevel> ReliquaryMainAffixLevels { get; set; }
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Reliquary;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataListReliquarySource
{
public List<Reliquary> Reliquaries { get; set; }
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Model.Metadata.Weapon;
@@ -19,39 +18,19 @@ internal static class MetadataServiceContextExtension
// List
{
if (context is IMetadataListAchievementSource listAchievementSource)
if (context is IMetadataListMaterialSource listMaterialSource)
{
listAchievementSource.Achievements = await metadataService.GetAchievementListAsync(token).ConfigureAwait(false);
listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false);
}
if (context is IMetadataListGachaEventSource listGachaEventSource)
{
listGachaEventSource.GachaEvents = await metadataService.GetGachaEventListAsync(token).ConfigureAwait(false);
}
if (context is IMetadataListMaterialSource listMaterialSource)
{
listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false);
}
if (context is IMetadataListReliquaryMainAffixLevelSource listReliquaryMainAffixLevelSource)
{
listReliquaryMainAffixLevelSource.ReliquaryMainAffixLevels = await metadataService.GetReliquaryMainAffixLevelListAsync(token).ConfigureAwait(false);
}
if (context is IMetadataListReliquarySource listReliquarySource)
{
listReliquarySource.Reliquaries = await metadataService.GetReliquaryListAsync(token).ConfigureAwait(false);
}
}
// Dictionary
{
if (context is IMetadataDictionaryIdAchievementSource dictionaryIdAchievementSource)
{
dictionaryIdAchievementSource.IdAchievementMap = await metadataService.GetIdToAchievementMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdAvatarSource dictionaryIdAvatarSource)
{
dictionaryIdAvatarSource.IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
@@ -62,21 +41,6 @@ internal static class MetadataServiceContextExtension
dictionaryIdMaterialSource.IdMaterialMap = await metadataService.GetIdToMaterialMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdReliquaryAffixWeightSource dictionaryIdReliquaryAffixWeightSource)
{
dictionaryIdReliquaryAffixWeightSource.IdReliquaryAffixWeightMap = await metadataService.GetIdToReliquaryAffixWeightMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdReliquaryMainPropertySource dictionaryIdReliquaryMainPropertySource)
{
dictionaryIdReliquaryMainPropertySource.IdReliquaryMainPropertyMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdReliquarySubAffixSource dictionaryIdReliquarySubAffixSource)
{
dictionaryIdReliquarySubAffixSource.IdReliquarySubAffixMap = await metadataService.GetIdToReliquarySubAffixMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdWeaponSource dictionaryIdWeaponSource)
{
dictionaryIdWeaponSource.IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);

View File

@@ -80,7 +80,7 @@ internal static class MetadataServiceListExtension
return metadataService.FromCacheOrFileAsync<List<ReliquaryMainAffix>>(FileNameReliquaryMainAffix, token);
}
public static ValueTask<List<ReliquaryMainAffixLevel>> GetReliquaryMainAffixLevelListAsync(this IMetadataService metadataService, CancellationToken token = default)
public static ValueTask<List<ReliquaryMainAffixLevel>> GetReliquaryLevelListAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheOrFileAsync<List<ReliquaryMainAffixLevel>>(FileNameReliquaryMainAffixLevel, token);
}

View File

@@ -16,22 +16,31 @@ internal sealed partial class SignInService : ISignInService
public async ValueTask<ValueResult<bool, string>> ClaimRewardAsync(UserAndUid userAndUid, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
ISignInClient signInClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<ISignInClient>>()
.Create(userAndUid.User.IsOversea);
Response<Reward> rewardResponse = await signInClient.GetRewardAsync(userAndUid.User, token).ConfigureAwait(false);
if (rewardResponse.IsOk())
{
ISignInClient signInClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<ISignInClient>>()
.Create(userAndUid.User.IsOversea);
Response<Reward> rewardResponse = await signInClient.GetRewardAsync(userAndUid.User, token).ConfigureAwait(false);
if (!rewardResponse.IsOk())
{
return new(false, SH.ServiceSignInRewardListRequestFailed);
}
Response<SignInResult> resultResponse = await signInClient.SignAsync(userAndUid, token).ConfigureAwait(false);
if (!resultResponse.IsOk(showInfoBar: false))
if (resultResponse.IsOk(showInfoBar: false))
{
Response<SignInRewardInfo> infoResponse = await signInClient.GetInfoAsync(userAndUid, token).ConfigureAwait(false);
if (infoResponse.IsOk())
{
int index = infoResponse.Data.TotalSignDay - 1;
Award award = rewardResponse.Data.Awards[index];
return new(true, SH.FormatServiceSignInSuccessRewardFormat(award.Name, award.Count));
}
else
{
return new(false, SH.ServiceSignInInfoRequestFailed);
}
}
else
{
string message = resultResponse.Message;
@@ -47,16 +56,10 @@ internal sealed partial class SignInService : ISignInService
return new(false, SH.FormatServiceSignInClaimRewardFailedFormat(message));
}
Response<SignInRewardInfo> infoResponse = await signInClient.GetInfoAsync(userAndUid, token).ConfigureAwait(false);
if (!infoResponse.IsOk())
{
return new(false, SH.ServiceSignInInfoRequestFailed);
}
int index = infoResponse.Data.TotalSignDay - 1;
Award award = rewardResponse.Data.Awards[index];
return new(true, SH.FormatServiceSignInSuccessRewardFormat(award.Name, award.Count));
}
else
{
return new(false, SH.ServiceSignInRewardListRequestFailed);
}
}
}

View File

@@ -21,9 +21,8 @@ namespace Snap.Hutao.Service.SpiralAbyss;
[Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordService))]
internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordService
{
//private readonly IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory;
private readonly IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory;
private readonly ISpiralAbyssRecordDbService spiralAbyssRecordDbService;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IMetadataService metadataService;
private readonly ITaskContext taskContext;
@@ -77,69 +76,54 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
/// <inheritdoc/>
public async ValueTask RefreshSpiralAbyssAsync(UserAndUid userAndUid)
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory = scope.ServiceProvider.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>();
// request the index first
await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetPlayerInfoAsync(userAndUid)
.ConfigureAwait(false);
// request the index first
await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetPlayerInfoAsync(userAndUid)
.ConfigureAwait(false);
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Last).ConfigureAwait(false);
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Current).ConfigureAwait(false);
}
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Last).ConfigureAwait(false);
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Current).ConfigureAwait(false);
}
private async ValueTask RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
{
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
if (response.IsOk())
{
IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory = scope.ServiceProvider.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>();
Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data;
response = await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(spiralAbysses);
ArgumentNullException.ThrowIfNull(metadataContext);
int index = spiralAbysses.FirstIndexOf(s => s.ScheduleId == webSpiralAbyss.ScheduleId);
if (index >= 0)
{
await taskContext.SwitchToBackgroundAsync();
SpiralAbyssView view = spiralAbysses[index];
SpiralAbyssEntry targetEntry;
if (view.Entity is not null)
{
view.Entity.SpiralAbyss = webSpiralAbyss;
await spiralAbyssRecordDbService.UpdateSpiralAbyssEntryAsync(view.Entity).ConfigureAwait(false);
targetEntry = view.Entity;
}
else
{
SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss);
await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false);
targetEntry = newEntry;
}
await taskContext.SwitchToMainThreadAsync();
spiralAbysses.RemoveAt(index);
spiralAbysses.Insert(index, SpiralAbyssView.From(targetEntry, metadataContext));
}
}
if (!response.IsOk())
{
return;
}
Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data;
ArgumentNullException.ThrowIfNull(spiralAbysses);
ArgumentNullException.ThrowIfNull(metadataContext);
int index = spiralAbysses.FirstIndexOf(s => s.ScheduleId == webSpiralAbyss.ScheduleId);
if (index < 0)
{
return;
}
await taskContext.SwitchToBackgroundAsync();
SpiralAbyssView view = spiralAbysses[index];
SpiralAbyssEntry targetEntry;
if (view.Entity is not null)
{
view.Entity.SpiralAbyss = webSpiralAbyss;
await spiralAbyssRecordDbService.UpdateSpiralAbyssEntryAsync(view.Entity).ConfigureAwait(false);
targetEntry = view.Entity;
}
else
{
SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss);
await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false);
targetEntry = newEntry;
}
await taskContext.SwitchToMainThreadAsync();
spiralAbysses.RemoveAt(index);
spiralAbysses.Insert(index, SpiralAbyssView.From(targetEntry, metadataContext));
}
}

View File

@@ -68,7 +68,6 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
return false;
}
// TODO: sharing scope
if (!await TrySetUserLTokenAsync(user, token).ConfigureAwait(false))
{
return false;
@@ -101,17 +100,11 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
return true;
}
Response<LTokenWrapper> lTokenResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IPassportClient passportClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea);
lTokenResponse = await passportClient
.GetLTokenBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
}
Response<LTokenWrapper> lTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetLTokenBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
if (lTokenResponse.IsOk())
{
@@ -138,17 +131,11 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
}
}
Response<UidCookieToken> cookieTokenResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IPassportClient passportClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea);
cookieTokenResponse = await passportClient
.GetCookieAccountInfoBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
}
Response<UidCookieToken> cookieTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
if (cookieTokenResponse.IsOk())
{
@@ -170,17 +157,11 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
private async ValueTask<bool> TrySetUserUserInfoAsync(ViewModel.User.User user, CancellationToken token)
{
Response<UserFullInfoWrapper> response;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IUserClient userClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(user.IsOversea);
response = await userClient
.GetUserFullInfoAsync(user.Entity, token)
.ConfigureAwait(false);
}
Response<UserFullInfoWrapper> response = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(user.IsOversea)
.GetUserFullInfoAsync(user.Entity, token)
.ConfigureAwait(false);
if (response.IsOk())
{
@@ -195,16 +176,10 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
private async ValueTask<bool> TrySetUserUserGameRolesAsync(ViewModel.User.User user, CancellationToken token)
{
Response<ListWrapper<UserGameRole>> userGameRolesResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
BindingClient bindingClient = scope.ServiceProvider
.GetRequiredService<BindingClient>();
userGameRolesResponse = await bindingClient
.GetUserGameRolesOverseaAwareAsync(user.Entity, token)
.ConfigureAwait(false);
}
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await serviceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(user.Entity, token)
.ConfigureAwait(false);
if (userGameRolesResponse.IsOk())
{

View File

@@ -93,17 +93,11 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
public async ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user)
{
// TODO: 提醒其他组件此用户的Cookie已更改
Response<UidCookieToken> cookieTokenResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IPassportClient passportClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea);
cookieTokenResponse = await passportClient
.GetCookieAccountInfoBySTokenAsync(user)
.ConfigureAwait(false);
}
Response<UidCookieToken> cookieTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user)
.ConfigureAwait(false);
if (!cookieTokenResponse.IsOk())
{

View File

@@ -35,11 +35,11 @@
<Configurations>Debug;Release</Configurations>
<!--
Required for .NET 8 MSIX packaging
10.2.4.1 Security - Software Dependencies
Products may depend on non-integrated software (such as another product or module)
to deliver primary functionality only as long as the additional required software
is disclosed within the first two lines of the description in the Store.
is disclosed within the first two lines of the description in the Store.
-->
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
@@ -52,7 +52,7 @@
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
</ItemGroup>
<!-- Included Files -->
<ItemGroup>
<None Remove="Assets\LargeTile.scale-100.png" />
@@ -205,7 +205,7 @@
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
</ItemGroup>
<!-- Analyzer Files -->
<ItemGroup>
<AdditionalFiles Include="CodeMetricsConfig.txt" />
@@ -220,7 +220,7 @@
<AdditionalFiles Include="Resource\Localization\SH.ru.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
</ItemGroup>
<!-- Assets Files -->
<ItemGroup>
<Content Update="Assets\Logo.ico" />
@@ -389,37 +389,37 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Theme\ScrollViewer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Theme\FlyoutStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportUnregisterDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportResetPasswordDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportRegisterDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportLoginDialog.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -569,13 +569,13 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="IdentifyMonitorWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Control\HutaoStatisticsCard.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -617,7 +617,7 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Pages -->
<ItemGroup>
<Page Update="View\Dialog\LaunchGamePackageConvertDialog.xaml">

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -16,7 +15,7 @@ internal sealed partial class AchievementCard : Button
/// </summary>
public AchievementCard()
{
this.InitializeDataContext<ViewModel.Achievement.AchievementViewModelSlim>();
DataContext = Ioc.Default.GetRequiredService<ViewModel.Achievement.AchievementViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -16,7 +15,7 @@ internal sealed partial class DailyNoteCard : Button
/// </summary>
public DailyNoteCard()
{
this.InitializeDataContext<ViewModel.DailyNote.DailyNoteViewModelSlim>();
DataContext = Ioc.Default.GetRequiredService<ViewModel.DailyNote.DailyNoteViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -16,7 +15,7 @@ internal sealed partial class GachaStatisticsCard : Button
/// </summary>
public GachaStatisticsCard()
{
this.InitializeDataContext<ViewModel.GachaLog.GachaLogViewModelSlim>();
DataContext = Ioc.Default.GetRequiredService<ViewModel.GachaLog.GachaLogViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -16,7 +15,7 @@ internal sealed partial class LaunchGameCard : Button
/// </summary>
public LaunchGameCard()
{
this.InitializeDataContext<ViewModel.Game.LaunchGameViewModelSlim>();
DataContext = Ioc.Default.GetRequiredService<ViewModel.Game.LaunchGameViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -34,11 +34,11 @@ internal sealed partial class AchievementImportDialog : ContentDialog
/// 异步获取导入选项
/// </summary>
/// <returns>导入选项</returns>
public async ValueTask<ValueResult<bool, ImportStrategyKind>> GetImportStrategyAsync()
public async ValueTask<ValueResult<bool, ImportStrategy>> GetImportStrategyAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
ImportStrategyKind strategy = (ImportStrategyKind)ImportModeSelector.SelectedIndex;
ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex;
return new(result == ContentDialogResult.Primary, strategy);
}

View File

@@ -14,7 +14,7 @@ internal sealed partial class GuideView : UserControl
{
public GuideView()
{
this.InitializeDataContext<GuideViewModel>();
InitializeComponent();
DataContext = this.ServiceProvider().GetRequiredService<GuideViewModel>();
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.View.Page;
using Snap.Hutao.ViewModel;
@@ -24,11 +23,12 @@ internal sealed partial class MainView : UserControl
{
IServiceProvider serviceProvider = Ioc.Default;
this.InitializeDataContext<MainViewModel>(serviceProvider);
MainViewModel mainViewModel = serviceProvider.GetRequiredService<MainViewModel>();
DataContext = mainViewModel;
InitializeComponent();
(DataContext as MainViewModel)?.Initialize(new BackgroundImagePresenterAccessor(BackgroundImagePresenter));
mainViewModel.Initialize(new BackgroundImagePresenterAccessor(BackgroundImagePresenter));
navigationService = serviceProvider.GetRequiredService<INavigationService>();
if (navigationService is INavigationInitialization navigationInitialization)

View File

@@ -475,18 +475,24 @@
Margin="16"
CornerRadius="{ThemeResource ControlCornerRadius}"
IsLoading="{Binding HutaoCloudStatisticsViewModel.IsInitialized, Converter={StaticResource BoolNegationConverter}}"/>
<shcp:HorizontalEqualPanel
<Grid
Margin="16"
Spacing="16"
ColumnSpacing="16"
Visibility="{Binding HutaoCloudStatisticsViewModel.IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding HutaoCloudStatisticsViewModel.OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<shvc:HutaoStatisticsCard DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent}"/>
<shvc:HutaoStatisticsCard DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent2}"/>
<shvc:HutaoStatisticsCard DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.WeaponEvent}"/>
<shvc:HutaoStatisticsCard DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.Chronicled}" Visibility="{Binding Converter={StaticResource EmptyObjectToVisibilityConverter}, FallbackValue=Collapsed}"/>
</shcp:HorizontalEqualPanel>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:HutaoStatisticsCard Grid.Column="0" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent}"/>
<shvc:HutaoStatisticsCard Grid.Column="1" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent2}"/>
<shvc:HutaoStatisticsCard Grid.Column="2" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.WeaponEvent}"/>
<shvc:HutaoStatisticsCard Grid.Column="3" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.Chronicled}"/>
</Grid>
</Grid>
</PivotItem>
</Pivot>

View File

@@ -560,11 +560,11 @@
OnContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOn}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingUnobtainedWishItemVisibleDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingUnobtainedWishItemVisibleHeader}"
Description="{shcm:ResourceString Name=ViewPageSettingNeverHeldItemVisibleDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingNeverHeldItemVisibleHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE890;}">
<ToggleSwitch
IsOn="{Binding AppOptions.IsUnobtainedWishItemVisible, Mode=TwoWay}"
IsOn="{Binding AppOptions.IsNeverHeldStatisticsItemVisible, Mode=TwoWay}"
OffContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOff}"
OnContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOn}"/>
</cwc:SettingsCard>

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View;
@@ -16,7 +15,7 @@ internal sealed partial class TitleView : UserControl
{
public TitleView()
{
this.InitializeDataContext<TitleViewModel>();
DataContext = Ioc.Default.GetRequiredService<TitleViewModel>();
InitializeComponent();
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.ViewModel.User;
namespace Snap.Hutao.View;
@@ -18,7 +17,7 @@ internal sealed partial class UserView : UserControl
/// </summary>
public UserView()
{
this.InitializeDataContext<UserViewModel>();
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<UserViewModel>();
}
}

View File

@@ -1,6 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.Abstraction;
internal interface IPageScoped;

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.ViewModel.Abstraction;
/// 视图模型接口
/// </summary>
[HighQuality]
internal interface IViewModel : IPageScoped
internal interface IViewModel
{
/// <summary>
/// 用于通知页面卸载的取消令牌

View File

@@ -7,16 +7,29 @@ using Snap.Hutao.Service.Navigation;
namespace Snap.Hutao.ViewModel.Abstraction;
/// <summary>
/// 简化的视图模型抽象类
/// </summary>
[ConstructorGenerated]
internal abstract partial class ViewModelSlim : ObservableObject
{
private readonly IServiceProvider serviceProvider;
private bool isInitialized;
/// <summary>
/// 是否初始化完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <summary>
/// 服务提供器
/// </summary>
protected IServiceProvider ServiceProvider { get => serviceProvider; }
/// <summary>
/// 打开界面执行
/// </summary>
/// <returns>任务</returns>
[Command("OpenUICommand")]
protected virtual Task OpenUIAsync()
{
@@ -24,10 +37,18 @@ internal abstract partial class ViewModelSlim : ObservableObject
}
}
/// <summary>
/// 简化的视图模型抽象类
/// 可导航
/// </summary>
/// <typeparam name="TPage">要导航到的页面类型</typeparam>
[ConstructorGenerated(CallBaseConstructor = true)]
internal abstract partial class ViewModelSlim<TPage> : ViewModelSlim
where TPage : Page
{
/// <summary>
/// 导航到指定的页面类型
/// </summary>
[Command("NavigateCommand")]
protected virtual void Navigate()
{

View File

@@ -7,9 +7,16 @@ using System.Runtime.InteropServices;
namespace Snap.Hutao.ViewModel.Achievement;
/// <summary>
/// 成就完成进度
/// </summary>
[HighQuality]
internal static class AchievementFinishPercent
{
/// <summary>
/// 更新完成进度
/// </summary>
/// <param name="viewModel">视图模型</param>
public static void Update(AchievementViewModel viewModel)
{
int totalFinished = 0;

View File

@@ -89,7 +89,7 @@ internal sealed partial class AchievementImporter
AchievementImportDialog importDialog = await dependencies.ContentDialogFactory
.CreateInstanceAsync<AchievementImportDialog>(uiaf).ConfigureAwait(false);
(bool isOk, ImportStrategyKind strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(false);
(bool isOk, ImportStrategy strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(false);
if (!isOk)
{

View File

@@ -3,13 +3,11 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Collection.AdvancedCollectionView;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Service.Achievement;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.View.Dialog;
@@ -160,19 +158,19 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
if (isOk)
{
ArchiveAddResultKind result = await dependencies.AchievementService.AddArchiveAsync(EntityAchievementArchive.From(name)).ConfigureAwait(false);
ArchiveAddResult result = await dependencies.AchievementService.AddArchiveAsync(EntityAchievementArchive.From(name)).ConfigureAwait(false);
switch (result)
{
case ArchiveAddResultKind.Added:
case ArchiveAddResult.Added:
await dependencies.TaskContext.SwitchToMainThreadAsync();
SelectedArchive = dependencies.AchievementService.CurrentArchive;
dependencies.InfoBarService.Success(SH.FormatViewModelAchievementArchiveAdded(name));
break;
case ArchiveAddResultKind.InvalidName:
case ArchiveAddResult.InvalidName:
dependencies.InfoBarService.Warning(SH.ViewModelAchievementArchiveInvalidName);
break;
case ArchiveAddResultKind.AlreadyExists:
case ArchiveAddResult.AlreadyExists:
dependencies.InfoBarService.Warning(SH.FormatViewModelAchievementArchiveAlreadyExists(name));
break;
default:
@@ -266,11 +264,9 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
return;
}
AchievementServiceMetadataContext context = await dependencies.MetadataService
.GetContextAsync<AchievementServiceMetadataContext>(CancellationToken)
.ConfigureAwait(false);
List<MetadataAchievement> achievements = await dependencies.MetadataService.GetAchievementListAsync(CancellationToken).ConfigureAwait(false);
if (TryGetAchievements(archive, context, out List<AchievementView>? combined))
if (TryGetAchievements(archive, achievements, out List<AchievementView>? combined))
{
await dependencies.TaskContext.SwitchToMainThreadAsync();
@@ -281,14 +277,14 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
}
}
private bool TryGetAchievements(EntityAchievementArchive archive, AchievementServiceMetadataContext context, [NotNullWhen(true)] out List<AchievementView>? combined)
private bool TryGetAchievements(EntityAchievementArchive archive, List<MetadataAchievement> achievements, [NotNullWhen(true)] out List<AchievementView>? combined)
{
try
{
combined = dependencies.AchievementService.GetAchievementViewList(archive, context);
combined = dependencies.AchievementService.GetAchievementViewList(archive, achievements);
return true;
}
catch (HutaoException ex)
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
dependencies.InfoBarService.Error(ex);
combined = default;

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Achievement;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
namespace Snap.Hutao.ViewModel.Achievement;
@@ -32,12 +31,12 @@ internal sealed partial class AchievementViewModelSlim : Abstraction.ViewModelSl
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
AchievementServiceMetadataContext context = await metadataService
.GetContextAsync<AchievementServiceMetadataContext>()
Dictionary<AchievementId, Model.Metadata.Achievement.Achievement> achievementMap = await metadataService
.GetIdToAchievementMapAsync()
.ConfigureAwait(false);
List<AchievementStatistics> list = await scope.ServiceProvider
.GetRequiredService<IAchievementStatisticsService>()
.GetAchievementStatisticsAsync(context)
.GetAchievementStatisticsAsync(achievementMap)
.ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();

View File

@@ -26,5 +26,5 @@ internal sealed class HutaoStatistics
/// <summary>
/// 集录祈愿
/// </summary>
public HutaoWishSummary? Chronicled { get; set; }
public HutaoWishSummary Chronicled { get; set; } = default!;
}

View File

@@ -41,7 +41,7 @@ internal sealed partial class LaunchGameShared
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
HutaoException.Throw($"不支持的 ChannelOptions: {options}");
HutaoException.Throw(HutaoExceptionKind.GameConfigInvalidChannelOptions, $"不支持的 ChannelOptions: {options}");
}
}

View File

@@ -8,7 +8,6 @@ using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.ViewModel.SpiralAbyss;
// TODO: replace this
internal sealed class SpiralAbyssMetadataContext
{
public Dictionary<TowerScheduleId, TowerSchedule> IdScheduleMap { get; set; } = default!;

View File

@@ -259,35 +259,31 @@ internal class MiHoYoJSBridge
protected virtual async ValueTask<JsResult<Dictionary<string, object>>> GetUserInfoAsync(JsParam param)
{
Response<UserFullInfoWrapper> response;
using (IServiceScope scope = serviceProvider.CreateScope())
Response<UserFullInfoWrapper> response = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(userAndUid.User.IsOversea)
.GetUserFullInfoAsync(userAndUid.User)
.ConfigureAwait(false);
if (response.IsOk())
{
IUserClient userClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(userAndUid.User.IsOversea);
response = await userClient
.GetUserFullInfoAsync(userAndUid.User)
.ConfigureAwait(false);
UserInfo info = response.Data.UserInfo;
return new()
{
Data = new()
{
["id"] = info.Uid,
["gender"] = info.Gender,
["nickname"] = info.Nickname,
["introduce"] = info.Introduce,
["avatar_url"] = info.AvatarUrl,
},
};
}
if (!response.IsOk())
else
{
return new();
}
UserInfo info = response.Data.UserInfo;
return new()
{
Data = new()
{
["id"] = info.Uid,
["gender"] = info.Gender,
["nickname"] = info.Nickname,
["introduce"] = info.Introduce,
["avatar_url"] = info.AvatarUrl,
},
};
}
protected virtual async ValueTask<IJsBridgeResult?> PushPageAsync(JsParam<PushPagePayload> param)

View File

@@ -15,6 +15,9 @@ using System.Net.Sockets;
namespace Snap.Hutao.Web.Enka;
/// <summary>
/// Enka API 客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
@@ -26,11 +29,23 @@ internal sealed partial class EnkaClient
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <summary>
/// 异步获取转发的 Enka API 响应
/// </summary>
/// <param name="playerUid">玩家Uid</param>
/// <param name="token">取消令牌</param>
/// <returns>Enka API 响应</returns>
public ValueTask<EnkaResponse?> GetForwardDataAsync(in PlayerUid playerUid, CancellationToken token = default)
{
return TryGetEnkaResponseCoreAsync(HutaoEndpoints.Enka(playerUid), token);
}
/// <summary>
/// 异步获取 Enka API 响应
/// </summary>
/// <param name="playerUid">玩家Uid</param>
/// <param name="token">取消令牌</param>
/// <returns>Enka API 响应</returns>
public ValueTask<EnkaResponse?> GetDataAsync(in PlayerUid playerUid, CancellationToken token = default)
{
return TryGetEnkaResponseCoreAsync(string.Format(CultureInfo.CurrentCulture, EnkaAPI, playerUid), token);

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.DataSigning;
namespace Snap.Hutao.Web.Hoyolab.Annotation;
/// <summary>
/// API 信息
/// 指示此API 已经经过验证,且明确其调用
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
internal sealed class ApiInformationAttribute : Attribute
{
/// <summary>
/// Cookie类型
/// </summary>
public CookieType Cookie { get; set; }
/// <summary>
/// SALT
/// </summary>
public SaltType Salt { get; set; }
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Request.Builder;
@@ -12,6 +13,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.App.Account;
/// <summary>
/// 账户客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc)]
@@ -21,6 +25,14 @@ internal sealed partial class AccountClient
private readonly ILogger<AccountClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步生成米游社操作验证密钥
/// </summary>
/// <param name="user">用户</param>
/// <param name="data">提交数据</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)]
public async ValueTask<Response<GameAuthKey>> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -9,6 +9,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// <summary>
/// 用户信息客户端 DS版
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc)]
@@ -18,6 +21,12 @@ internal sealed partial class UserClient : IUserClient
private readonly ILogger<UserClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 获取当前用户详细信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
public async ValueTask<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
ArgumentException.ThrowIfNullOrEmpty(user.Aid);

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
@@ -9,6 +10,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// <summary>
/// 用户信息客户端 Hoyolab版
/// </summary>
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class UserClientOversea : IUserClient
@@ -17,6 +21,13 @@ internal sealed partial class UserClientOversea : IUserClient
private readonly ILogger<UserClientOversea> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 获取当前用户详细信息,使用 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.LToken)]
public async ValueTask<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
ArgumentException.ThrowIfNullOrEmpty(user.Aid);

View File

@@ -19,10 +19,12 @@ internal sealed partial class PandaClient
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
{
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40);
// Use 12 (zzz) instead of 4 (gi) temporarily to get legacy game token
GameLoginRequest options = GameLoginRequest.Create(12, HoyolabOptions.DeviceId40);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeFetch)
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
.PostJson(options);
Response<UrlWrapper>? resp = await builder
@@ -34,10 +36,11 @@ internal sealed partial class PandaClient
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
{
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket);
GameLoginRequest options = GameLoginRequest.Create(12, HoyolabOptions.DeviceId40, ticket);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeQuery)
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
.PostJson(options);
Response<GameLoginResult>? resp = await builder

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
@@ -11,6 +12,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端 XRPC 版
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc2)]
@@ -20,6 +24,13 @@ internal sealed partial class PassportClient : IPassportClient
private readonly ILogger<PassportClient2> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步获取 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async ValueTask<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -36,6 +47,13 @@ internal sealed partial class PassportClient : IPassportClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async ValueTask<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
@@ -12,6 +13,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc2)]
@@ -21,6 +25,13 @@ internal sealed partial class PassportClient2
private readonly ILogger<PassportClient2> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步验证 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>验证信息</returns>
[ApiInformation(Cookie = CookieType.LToken)]
public async ValueTask<Response<UserInfoWrapper>> VerifyLtokenAsync(User user, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -35,6 +46,13 @@ internal sealed partial class PassportClient2
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async ValueTask<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -61,6 +79,7 @@ internal sealed partial class PassportClient2
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken)
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
.PostJson(data);
Response<LoginResult>? resp = await builder

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
@@ -10,6 +11,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端 XRPC 版
/// </summary>
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc3)]
internal sealed partial class PassportClientOversea : IPassportClient
@@ -18,6 +22,13 @@ internal sealed partial class PassportClientOversea : IPassportClient
private readonly ILogger<PassportClientOversea> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步获取 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken)]
public async ValueTask<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
{
string? stoken = user.SToken?.GetValueOrDefault(Cookie.STOKEN);
@@ -37,6 +48,13 @@ internal sealed partial class PassportClientOversea : IPassportClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken)]
public async ValueTask<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
string? stoken = user.SToken?.GetValueOrDefault(Cookie.STOKEN);

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Request.Builder;
@@ -12,6 +13,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
/// <summary>
/// 授权客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
@@ -21,6 +25,7 @@ internal sealed partial class AuthClient
private readonly ILogger<BindingClient> logger;
private readonly HttpClient httpClient;
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)]
public async ValueTask<Response<ActionTicketWrapper>> GetActionTicketBySTokenAsync(string action, User user, CancellationToken token = default)
{
ArgumentException.ThrowIfNullOrEmpty(user.Aid);
@@ -40,6 +45,13 @@ internal sealed partial class AuthClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取 MultiToken
/// </summary>
/// <param name="cookie">login cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="token">取消令牌</param>
/// <returns>包含token的字典</returns>
public async ValueTask<Response<ListWrapper<NameToken>>> GetMultiTokenByLoginTicketAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
{
Response<ListWrapper<NameToken>>? resp = null;

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
@@ -11,6 +12,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// 绑定客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
@@ -21,6 +25,13 @@ internal sealed partial class BindingClient
private readonly ILogger<BindingClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步获取用户角色信息
/// 自动判断是否为国际服
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
public async ValueTask<Response<ListWrapper<UserGameRole>>> GetUserGameRolesOverseaAwareAsync(User user, CancellationToken token = default)
{
if (user.IsOversea)
@@ -44,6 +55,14 @@ internal sealed partial class BindingClient
}
}
/// <summary>
/// 异步获取用户角色信息
/// </summary>
/// <param name="actionTicket">操作凭证</param>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.LToken)]
public async ValueTask<Response<ListWrapper<UserGameRole>>> GetUserGameRolesByActionTicketAsync(string actionTicket, User user, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -58,6 +77,13 @@ internal sealed partial class BindingClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取国际服用户角色信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.LToken)]
public async ValueTask<Response<ListWrapper<UserGameRole>>> GetOverseaUserGameRolesByCookieAsync(User user, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
@@ -11,6 +12,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// SToken绑定客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc)]
@@ -21,6 +25,13 @@ internal sealed partial class BindingClient2
private readonly ILogger<BindingClient2> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 获取用户角色信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.LK2)]
public async ValueTask<Response<ListWrapper<UserGameRole>>> GetUserGameRolesBySTokenAsync(User user, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -38,6 +49,15 @@ internal sealed partial class BindingClient2
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步生成祈愿验证密钥
/// 需要 SToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="data">提交数据</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.LK2)]
public async ValueTask<Response<GameAuthKey>> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
@@ -10,6 +11,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
/// <summary>
/// 养成计算器客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.Default)]
@@ -19,6 +23,14 @@ internal sealed partial class CalculateClient
private readonly ILogger<CalculateClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步计算结果
/// </summary>
/// <param name="user">用户</param>
/// <param name="delta">差异</param>
/// <param name="token">取消令牌</param>
/// <returns>消耗结果</returns>
[ApiInformation(Cookie = CookieType.Cookie)]
public async ValueTask<Response<Consumption>> ComputeAsync(Model.Entity.User user, AvatarPromotionDelta delta, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -34,6 +46,12 @@ internal sealed partial class CalculateClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取角色列表
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async ValueTask<List<Avatar>> GetAvatarsAsync(UserAndUid userAndUid, CancellationToken token = default)
{
int currentPage = 1;
@@ -73,6 +91,13 @@ internal sealed partial class CalculateClient
return avatars;
}
/// <summary>
/// 异步获取角色详情
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="avatar">角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色详情</returns>
public async ValueTask<Response<AvatarDetail>> GetAvatarDetailAsync(UserAndUid userAndUid, Avatar avatar, CancellationToken token = default)
{
string url = userAndUid.User.IsOversea
@@ -92,6 +117,13 @@ internal sealed partial class CalculateClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取摹本的家具列表
/// </summary>
/// <param name="user">用户</param>
/// <param name="shareCode">摹本码</param>
/// <param name="token">取消令牌</param>
/// <returns>家具列表</returns>
public async ValueTask<Response<FurnitureListWrapper>> FurnitureBlueprintAsync(Model.Entity.User user, string shareCode, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -107,6 +139,13 @@ internal sealed partial class CalculateClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 家具数量计算
/// </summary>
/// <param name="user">用户</param>
/// <param name="items">物品</param>
/// <param name="token">取消令牌</param>
/// <returns>消耗</returns>
public async ValueTask<Response<ListWrapper<Item>>> FurnitureComputeAsync(Model.Entity.User user, List<Item> items, CancellationToken token)
{
ListWrapper<IdCount> data = new() { List = items.Select(i => new IdCount { Id = i.Id, Count = i.Num }).ToList() };

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Verification;
using Snap.Hutao.Web.Request.Builder;
@@ -12,6 +13,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 卡片客户端
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc)]
@@ -25,7 +29,7 @@ internal sealed partial class CardClient
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.CardCreateVerification(true))
.SetUserCookieAndFpHeader(user, CookieType.Cookie)
.SetUserCookieAndFpHeader(user, CookieType.LToken)
.SetHeader("x-rpc-challenge_game", $"{headers.ChallengeGame}")
.SetHeader("x-rpc-challenge_path", headers.ChallengePath)
.Get();
@@ -39,11 +43,10 @@ internal sealed partial class CardClient
return Response.Response.DefaultIfNull(resp);
}
public async ValueTask<Response<VerificationResult>> VerifyVerificationAsync(User user, CardVerifiationHeaders headers, string challenge, string validate, CancellationToken token)
public async ValueTask<Response<VerificationResult>> VerifyVerificationAsync(CardVerifiationHeaders headers, string challenge, string validate, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.CardVerifyVerification)
.SetUserCookieAndFpHeader(user, CookieType.Cookie)
.SetHeader("x-rpc-challenge_game", $"{headers.ChallengeGame}")
.SetHeader("x-rpc-challenge_path", headers.ChallengePath)
.PostJson(new VerificationData(challenge, validate));
@@ -57,6 +60,13 @@ internal sealed partial class CardClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取桌面小组件数据
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>桌面小组件数据</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.X6)]
public async ValueTask<Response<DailyNote.WidgetDailyNote>> GetWidgetDataAsync(User user, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Verification;
@@ -13,6 +14,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 游戏记录提供器
/// </summary>
[HighQuality]
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc)]
@@ -24,6 +28,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient
private readonly ILogger<GameRecordClient> logger;
private readonly HttpClient httpClient;
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.X4)]
public async ValueTask<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -67,6 +72,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
public async ValueTask<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -110,6 +116,14 @@ internal sealed partial class GameRecordClient : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="userAndUid">用户</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.X4)]
public async ValueTask<Response<SpiralAbyss.SpiralAbyss>> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -153,6 +167,13 @@ internal sealed partial class GameRecordClient : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取角色基本信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色基本信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
public async ValueTask<Response<BasicRoleInfo>> GetRoleBasicInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -170,6 +191,14 @@ internal sealed partial class GameRecordClient : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
public async ValueTask<Response<CharacterWrapper>> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Request.Builder;
@@ -12,6 +13,9 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// Hoyoverse game record provider
/// </summary>
[ConstructorGenerated(ResolveHttpClient = true)]
[HttpClient(HttpClientConfiguration.XRpc3)]
[PrimaryHttpMessageHandler(UseCookies = false)]
@@ -21,6 +25,13 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient
private readonly ILogger<GameRecordClient> logger;
private readonly HttpClient httpClient;
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSX4)]
public async ValueTask<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -37,6 +48,13 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSX4)]
public async ValueTask<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -53,6 +71,14 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="userAndUid">用户</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSX4)]
public async ValueTask<Response<SpiralAbyss.SpiralAbyss>> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
@@ -69,6 +95,14 @@ internal sealed partial class GameRecordClientOversea : IGameRecordClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSX4)]
public async ValueTask<Response<CharacterWrapper>> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -24,7 +24,7 @@ internal sealed partial class HomaGeetestCardVerifier : IGeetestCardVerifier
if (response is { Code: 0, Data.Validate: string validate })
{
Response.Response<VerificationResult> verifyResponse = await cardClient.VerifyVerificationAsync(user, headers, registration.Challenge, validate, token).ConfigureAwait(false);
Response.Response<VerificationResult> verifyResponse = await cardClient.VerifyVerificationAsync(headers, registration.Challenge, validate, token).ConfigureAwait(false);
if (verifyResponse.IsOk())
{
VerificationResult result = verifyResponse.Data;