Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
a845dff6ee Revert "temporary fix qr login"
This reverts commit d4bd610fe2.
2024-04-14 13:55:16 +08:00
203 changed files with 2447 additions and 3279 deletions

View File

@@ -10,6 +10,8 @@ using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell; using Snap.Hutao.Core.Shell;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
namespace Snap.Hutao; namespace Snap.Hutao;

View File

@@ -28,21 +28,4 @@ internal static class FrameworkElementExtension
frameworkElement.IsRightTapEnabled = false; frameworkElement.IsRightTapEnabled = false;
frameworkElement.IsTabStop = 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 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. 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. 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. return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Windows.Foundation; using Windows.Foundation;

View File

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

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation; using Windows.Foundation;
namespace Snap.Hutao.Control.Panel; namespace Snap.Hutao.Control.Panel;

View File

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

View File

@@ -1,32 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Collection;
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
{
private readonly IEnumerator<TFirst> firstEnumerator;
private readonly IEnumerator<TSecond> secondEnumerator;
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
{
firstEnumerator = firstEnumerable.GetEnumerator();
secondEnumerator = secondEnumerable.GetEnumerator();
}
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
{
moveFirst = moveFirst && firstEnumerator.MoveNext();
moveSecond = moveSecond && secondEnumerator.MoveNext();
return moveFirst || moveSecond;
}
public void Dispose()
{
firstEnumerator.Dispose();
secondEnumerator.Dispose();
}
}

View File

@@ -13,6 +13,13 @@ namespace Snap.Hutao.Core.Database;
[HighQuality] [HighQuality]
internal static class DbSetExtension internal static class DbSetExtension
{ {
/// <summary>
/// 添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity) public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class where TEntity : class
{ {
@@ -20,13 +27,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker(); return dbSet.SaveChangesAndClearChangeTracker();
} }
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default) /// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class where TEntity : class
{ {
dbSet.Add(entity); dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token); return dbSet.SaveChangesAndClearChangeTrackerAsync();
} }
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities) public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class where TEntity : class
{ {
@@ -34,13 +55,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker(); return dbSet.SaveChangesAndClearChangeTracker();
} }
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default) /// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class where TEntity : class
{ {
dbSet.AddRange(entities); dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token); return dbSet.SaveChangesAndClearChangeTrackerAsync();
} }
/// <summary>
/// 移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity) public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class where TEntity : class
{ {
@@ -48,13 +83,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker(); return dbSet.SaveChangesAndClearChangeTracker();
} }
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default) /// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class where TEntity : class
{ {
dbSet.Remove(entity); dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token); return dbSet.SaveChangesAndClearChangeTrackerAsync();
} }
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity) public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class where TEntity : class
{ {
@@ -62,11 +111,18 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker(); return dbSet.SaveChangesAndClearChangeTracker();
} }
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default) /// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class where TEntity : class
{ {
dbSet.Update(entity); dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token); return dbSet.SaveChangesAndClearChangeTrackerAsync();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -80,11 +136,11 @@ internal static class DbSetExtension
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default) private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class where TEntity : class
{ {
DbContext dbContext = dbSet.Context(); DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false); int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
dbContext.ChangeTracker.Clear(); dbContext.ChangeTracker.Clear();
return count; return count;
} }

View File

@@ -33,17 +33,17 @@ internal static class IocConfiguration
return services return services
.AddTransient(typeof(Database.ScopedDbCurrent<,>)) .AddTransient(typeof(Database.ScopedDbCurrent<,>))
.AddTransient(typeof(Database.ScopedDbCurrent<,,>)) .AddTransient(typeof(Database.ScopedDbCurrent<,,>))
.AddDbContextPool<AppDbContext>(AddDbContextCore); .AddDbContext<AppDbContext>(AddDbContextCore);
} }
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder) private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
{ {
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>(); RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db"); string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}"; string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context // Temporarily create a context
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString)) using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
{ {
if (context.Database.GetPendingMigrations().Any()) if (context.Database.GetPendingMigrations().Any())
{ {

View File

@@ -1,7 +1,5 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core.Diagnostics; namespace Snap.Hutao.Core.Diagnostics;
internal readonly struct MeasureExecutionToken : IDisposable internal readonly struct MeasureExecutionToken : IDisposable
@@ -19,6 +17,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
public void Dispose() public void Dispose()
{ {
logger.LogColorizedDebug(("{Caller} toke {Time} ms", ConsoleColor.Gray), (callerName, ConsoleColor.Yellow), (stopwatch.GetElapsedTime().TotalMilliseconds, ConsoleColor.DarkGreen)); logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
} }
} }

View File

@@ -5,43 +5,46 @@ namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception 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) : base($"{message}\n{innerException?.Message}", innerException)
{ {
} }
public HutaoExceptionKind Kind { get; private set; }
[DoesNotReturn] [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) 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) if (!condition)
{ {
throw new HutaoException(message, innerException); throw new HutaoException(kind, message, innerException);
} }
} }
[DoesNotReturn] [DoesNotReturn]
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default) 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);
} }
[DoesNotReturn] [DoesNotReturn]
@@ -51,12 +54,6 @@ internal sealed class HutaoException : Exception
throw new InvalidCastException(message, innerException); throw new InvalidCastException(message, innerException);
} }
[DoesNotReturn]
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
{
throw new NotSupportedException(message, innerException);
}
[DoesNotReturn] [DoesNotReturn]
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default) public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
{ {

View File

@@ -9,8 +9,6 @@ internal enum HutaoExceptionKind
// Foundation // Foundation
ImageCacheInvalidUri, ImageCacheInvalidUri,
DatabaseCorrupted,
UserdataCorrupted,
// IO // IO
FileSystemCreateFileInsufficientPermissions, FileSystemCreateFileInsufficientPermissions,

View File

@@ -1,13 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using System.IO; using System.IO;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
using static Snap.Hutao.Win32.Shell32;
namespace Snap.Hutao.Core.IO; namespace Snap.Hutao.Core.IO;
@@ -45,57 +39,4 @@ internal static class FileOperation
File.Move(sourceFileName, destFileName, false); File.Move(sourceFileName, destFileName, false);
return true; return true;
} }
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(sourceFileName, default, in IShellItem.IID, out IShellItem* pSourceShellItem)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(destFileName, default, in IShellItem.IID, out IShellItem* pDestShellItem)))
{
pFileOperation->MoveItem(pSourceShellItem, pDestShellItem, default, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
pDestShellItem->Release();
}
pSourceShellItem->Release();
}
pFileOperation->Release();
}
return result;
}
public static unsafe bool UnsafeDelete(string path)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->DeleteItem(pShellItem, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
pShellItem->Release();
}
pFileOperation->Release();
}
return result;
}
} }

View File

@@ -10,7 +10,7 @@ internal static class Hash
{ {
public static string SHA1HexString(string input) public static string SHA1HexString(string input)
{ {
return HashCore(System.Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input); return HashCore(BitConverter.ToString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
} }
private static TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input) private static TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)

View File

@@ -1,40 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
internal sealed class StreamReaderWriter : IDisposable
{
private readonly StreamReader reader;
private readonly StreamWriter writer;
public StreamReaderWriter(StreamReader reader, StreamWriter writer)
{
this.reader = reader;
this.writer = writer;
}
public StreamReader Reader { get => reader; }
public StreamWriter Writer { get => writer; }
/// <inheritdoc cref="StreamReader.ReadLineAsync(CancellationToken)"/>
public ValueTask<string?> ReadLineAsync(CancellationToken token)
{
return reader.ReadLineAsync(token);
}
/// <inheritdoc cref="StreamWriter.WriteAsync(string?)"/>
public Task WriteAsync(string value)
{
return writer.WriteAsync(value);
}
public void Dispose()
{
writer.Dispose();
reader.Dispose();
}
}

View File

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

View File

@@ -49,7 +49,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
{ {
byte[] content = new byte[header->ContentLength]; byte[] content = new byte[header->ContentLength];
serverStream.ReadAtLeast(content, header->ContentLength, false); 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; return content;
} }

View File

@@ -21,11 +21,6 @@ internal readonly struct LogArgument
return new(argument); return new(argument);
} }
public static implicit operator LogArgument(double argument)
{
return new(argument);
}
public static implicit operator LogArgument((object? Argument, ConsoleColor Foreground) tuple) public static implicit operator LogArgument((object? Argument, ConsoleColor Foreground) tuple)
{ {
return new(tuple.Argument, tuple.Foreground); return new(tuple.Argument, tuple.Foreground);

View File

@@ -21,27 +21,22 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync() public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{ {
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico"); string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
try try
{ {
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri(); Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri); StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false); await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
Uri elevatedLauncherUri = "ms-appx:///Snap.Hutao.Elevated.Launcher.exe".ToUri();
StorageFile launcherFile = await StorageFile.GetFileFromApplicationUriAsync(elevatedLauncherUri);
await launcherFile.OverwriteCopyAsync(elevatedLauncherPath).ConfigureAwait(false);
} }
catch catch
{ {
return false; return false;
} }
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath); return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
} }
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath) private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
{ {
bool result = false; bool result = false;
@@ -49,11 +44,17 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink); HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
if (SUCCEEDED(hr)) if (SUCCEEDED(hr))
{ {
pShellLink->SetPath(elevatedLauncherPath); pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
pShellLink->SetArguments(runtimeOptions.FamilyName);
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL); pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
pShellLink->SetIconLocation(targetLogoPath, 0); pShellLink->SetIconLocation(targetLogoPath, 0);
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
{
pShellLinkDataList->GetFlags(out uint flags);
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
pShellLinkDataList->Release();
}
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile))) if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
{ {
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

View File

@@ -118,6 +118,14 @@ internal static partial class EnumerableExtension
collection.RemoveAt(collection.Count - 1); 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] [Pure]
public static List<TResult> SelectList<TSource, TResult>(this List<TSource> list, Func<TSource, TResult> selector) 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. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Windowing; using Snap.Hutao.Core.Windowing;
using Snap.Hutao.ViewModel.Game; using Snap.Hutao.ViewModel.Game;
using Snap.Hutao.Win32.UI.WindowsAndMessaging; using Snap.Hutao.Win32.UI.WindowsAndMessaging;
@@ -36,7 +35,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
scope = serviceProvider.CreateScope(); scope = serviceProvider.CreateScope();
windowOptions = new(this, DragableGrid, new(MaxWidth, MaxHeight)); windowOptions = new(this, DragableGrid, new(MaxWidth, MaxHeight));
this.InitializeController(serviceProvider); this.InitializeController(serviceProvider);
RootGrid.InitializeDataContext<LaunchGameViewModel>(scope.ServiceProvider); RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
} }
/// <inheritdoc/> /// <inheritdoc/>

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 IAppDbEntity
{
Guid InnerId { get; }
}

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

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[Table("cultivate_entries")] [Table("cultivate_entries")]
internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry, CultivateType, uint>, IAppDbEntity internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry, CultivateType, uint>
{ {
/// <summary> /// <summary>
/// 内部Id /// 内部Id

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Abstraction;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@@ -14,7 +13,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[Table("cultivate_projects")] [Table("cultivate_projects")]
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>, IAppDbEntity internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>
{ {
/// <summary> /// <summary>
/// 内部Id /// 内部Id

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Model.Entity.Configuration; using Snap.Hutao.Model.Entity.Configuration;
using System.Diagnostics; using System.Diagnostics;
@@ -25,8 +24,18 @@ internal sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options) public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) : base(options)
{ {
logger = this.GetService<ILogger<AppDbContext>>(); }
logger?.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
/// <param name="options">选项</param>
/// <param name="logger">日志器</param>
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
: this(options)
{
this.logger = logger;
logger.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
} }
public DbSet<SettingEntry> Settings { get; set; } = default!; public DbSet<SettingEntry> Settings { get; set; } = default!;
@@ -65,14 +74,14 @@ internal sealed class AppDbContext : DbContext
public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!; public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!;
public static AppDbContext Create(IServiceProvider serviceProvider, string sqlConnectionString) /// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>应用程序数据库上下文</returns>
public static AppDbContext Create(string sqlConnectionString)
{ {
DbContextOptions<AppDbContext> options = new DbContextOptionsBuilder<AppDbContext>() return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
.UseApplicationServiceProvider(serviceProvider)
.UseSqlite(sqlConnectionString)
.Options;
return new(options);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -18,7 +18,7 @@ internal sealed class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactor
#if DEBUG #if DEBUG
// TODO: replace with your own database file path. // TODO: replace with your own database file path.
string userdataDbName = @"D:\Hutao\Userdata.db"; string userdataDbName = @"D:\Hutao\Userdata.db";
return AppDbContext.Create(default!, $"Data Source={userdataDbName}"); return AppDbContext.Create($"Data Source={userdataDbName}");
#else #else
throw Must.NeverHappen(); throw Must.NeverHappen();
#endif #endif

View File

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

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Intrinsic;
internal enum QuestType
{
AQ,
FQ,
LQ,
EQ,
DQ,
IQ,
VQ,
WQ,
}

View File

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

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using WinRT; using WinRT;

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Open UIAF Json File</value> <value>Open UIAF Json File</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>Multiple identical achievement IDs found in a single achievement archive</value> <value>Multiple identical achievement IDs found in a single achievement archive</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,19 +2922,13 @@
<value>Weapon WIKI</value> <value>Weapon WIKI</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Event Duration〓|〓Quest Start Time〓).*?Permanently.*?Version.*?(\d\.\d).*?update</value> <value>(?:〓Event Duration〓|〓Quest Start Time〓).*?\d\.\dthe Version update(?:after|)Permanently available</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Event Duration〓.*?Available throughout the entirety of Version (\d\.\d)</value> <value>〓Event Duration〓.*?\d\.\d Available throughout the entirety of Version</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】|〓Discount Period〓).*?After.*?(\d\.\d).*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】|〓Discount Period〓).*?(\d\.\dAfter the Version update).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>Dear.*?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>Version \d\.\d Update Maintenance Preview</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Update Maintenance Duration〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓Update Maintenance Duration〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Buka berkas UIAF Json</value> <value>Buka berkas UIAF Json</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>Terdapat beberapa ID pencapaian yang identik dalam satu arsip pencapaian</value> <value>Terdapat beberapa ID pencapaian yang identik dalam satu arsip pencapaian</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,19 +2922,13 @@
<value>Senjata WIKI</value> <value>Senjata WIKI</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Durasi Event〓|〓Waktu Mulai Misi〓).*?(\d\.\d)the Version update(?:after|)Selamanya Tersedia</value> <value>(?:〓Durasi Event〓|〓Waktu Mulai Misi〓).*?\d\.\dthe Version update(?:after|)Selamanya Tersedia</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Durasi Event〓.*?(\d\.\d) Tersedia selama versi ini</value> <value>〓Durasi Event〓.*?\d\.\d Tersedia selama versi ini</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Waktu Acara〓|Waktu Menginginkan|【Waktu Peluncuran】|〓Waktu Diskon〓).*?(\d\.\d) Setelah Pembaruan Versi.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓Waktu Acara〓|Waktu Menginginkan|【Waktu Peluncuran】|〓Waktu Diskon〓).*?(\d\.\d Setelah Pembaruan Versi).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Durasi Pemeliharaan Pembaruan.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓Durasi Pemeliharaan Pembaruan.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>UIAF Json ファイルを開く</value> <value>UIAF Json ファイルを開く</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>複数の同一アチーブメント Idがアーカイブに混在しています</value> <value>複数の同一アチーブメント Idがアーカイブに混在しています</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,25 +2922,19 @@
<value>武器一覧</value> <value>武器一覧</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓イベント期間〓|〓任務開(?:始|放)時間〓).*?(\d\.\d).*?開放</value> <value>(?:〓イベント期間〓|〓任務開時間〓).*?\d\.\dバージョンアップ(?:完了|)後常設オープン</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓イベント期間〓.*?(\d\.\d)バージョン</value> <value>〓イベント期間〓.*?\d\.\dバージョン期間オープン</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓イベント期間〓|祈願期間|【開始日時】).*?(\d\.\d)バージョンアップ.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓イベント期間〓|祈願期間|【開始日時】).*?(\d\.\dバージョンアップ完了後).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>親愛.*?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>Ver\.\d\.\dバージョンアップのお知らせ</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓メンテナンス時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓メンテナンス時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTitle" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTitle" xml:space="preserve">
<value>Ver\.\d\.\d.*?正式リリース</value> <value>Ver.\d\.\d.+正式リリース</value>
</data> </data>
<data name="WebAnnouncementTimeDaysBeginFormat" xml:space="preserve"> <data name="WebAnnouncementTimeDaysBeginFormat" xml:space="preserve">
<value>{0} 日後に開始</value> <value>{0} 日後に開始</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打开 UIAF Json 文件</value> <value>打开 UIAF Json 文件</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>한 업적 아카이브에서 Id 동일한 업적 발견됨</value> <value>한 업적 아카이브에서 Id 동일한 업적 발견됨</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,19 +2922,13 @@
<value>무기 자료</value> <value>무기 자료</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value> <value>(?:〓活动时间〓|〓任务开放时间〓).*?\d\.\d版本更新(?:完成|)后永久开放</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value> <value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓活动时间〓|祈愿时间|【上架时间】).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Abrir arquivo Json UIAF</value> <value>Abrir arquivo Json UIAF</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>Várias IDs de conquistas idênticas encontradas em um único arquivo de conquistas</value> <value>Várias IDs de conquistas idênticas encontradas em um único arquivo de conquistas</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,19 +2922,13 @@
<value>Wiki de armas</value> <value>Wiki de armas</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Duração do evento〓|〓Hora de início da missão〓).*?(\d\.\d)a atualização da versão(?:after|)Disponível permanentemente</value> <value>(?:〓Duração do evento〓|〓Hora de início da missão〓).*?\d\.\da atualização da versão(?:after|)Disponível permanentemente</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Duração do evento〓.*?(\d\.\d) Disponível em toda as versões</value> <value>〓Duração do evento〓.*?\d\.\d Disponível em toda as versões</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Duração do evento〓|Duração da oração do evento|【Duração da disponibilidade】).*?(\d\.\d)Após a atualização da versão.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓Duração do evento〓|Duração da oração do evento|【Duração da disponibilidade】).*?(\d\.\dApós a atualização da versão).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Duração da manutenção da atualização.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓Duração da manutenção da atualização.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打开 UIAF Json 文件</value> <value>打开 UIAF Json 文件</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>单个成就存档内发现多个相同的成就 Id</value> <value>单个成就存档内发现多个相同的成就 Id</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -1361,21 +1361,6 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve"> <data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value> <value>是否永久删除用户数据</value>
</data> </data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">
<value>上传深渊数据</value>
</data>
<data name="ViewDialogUpdatePackageDownloadUpdatelogLinkContent" xml:space="preserve">
<value>查看更新日志</value>
</data>
<data name="ViewDialogUserDocumentAction" xml:space="preserve"> <data name="ViewDialogUserDocumentAction" xml:space="preserve">
<value>立即前往</value> <value>立即前往</value>
</data> </data>
@@ -1736,9 +1721,6 @@
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve"> <data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
<value>无感验证复合 Url 配置成功</value> <value>无感验证复合 Url 配置成功</value>
</data> </data>
<data name="ViewModelSettingResetStaticResourceProgress" xml:space="preserve">
<value>正在重置图片资源</value>
</data>
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve"> <data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
<value>设置数据目录成功,重启以应用更改</value> <value>设置数据目录成功,重启以应用更改</value>
</data> </data>
@@ -2720,12 +2702,6 @@
<data name="ViewPageSettingTranslateNavigate" xml:space="preserve"> <data name="ViewPageSettingTranslateNavigate" xml:space="preserve">
<value>贡献翻译</value> <value>贡献翻译</value>
</data> </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"> <data name="ViewPageSettingUpdateCheckAction" xml:space="preserve">
<value>前往商店</value> <value>前往商店</value>
</data> </data>
@@ -2907,7 +2883,7 @@
<value>上传数据</value> <value>上传数据</value>
</data> </data>
<data name="ViewTitileUpdatePackageDownloadContent" xml:space="preserve"> <data name="ViewTitileUpdatePackageDownloadContent" xml:space="preserve">
<value>是否立即下载</value> <value>是否立即下载</value>
</data> </data>
<data name="ViewTitileUpdatePackageDownloadFailedMessage" xml:space="preserve"> <data name="ViewTitileUpdatePackageDownloadFailedMessage" xml:space="preserve">
<value>下载更新失败</value> <value>下载更新失败</value>
@@ -3006,19 +2982,13 @@
<value>武器资料</value> <value>武器资料</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value> <value>(?:〓活动时间〓|〓任务开放时间〓).*?\d\.\d版本更新(?:完成|)后永久开放</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value> <value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Открыть UIAF Json файл</value> <value>Открыть UIAF Json файл</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>В одном архиве достижений обнаружено несколько одинаковых идентификаторов достижений</value> <value>В одном архиве достижений обнаружено несколько одинаковых идентификаторов достижений</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,19 +2922,13 @@
<value>武器资料</value> <value>武器资料</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value> <value>(?:〓活动时间〓|〓任务开放时间〓).*?\d\.\d版本更新(?:完成|)后永久开放</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value> <value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓活动时间〓|祈愿时间|【上架时间】).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve"> <data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打開 UIAF Json 文件</value> <value>打開 UIAF Json 文件</value>
</data> </data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve"> <data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>單個成就存檔內發現多個相同的成就 Id</value> <value>單個成就存檔內發現多個相同的成就 Id</value>
</data> </data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve"> <data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2922,19 +2922,13 @@
<value>武器資料</value> <value>武器資料</value>
</data> </data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活動時間〓|〓任務開放時間〓).*?(\d\.\d)版本更新(?:完成|)後永久開放</value> <value>(?:〓活動時間〓|〓任務開放時間〓).*?\d\.\d版本更新(?:完成|)後永久開放</value>
</data> </data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活動時間〓.*?(\d\.\d)版本期間持續開放</value> <value>〓活動時間〓.*?\d\.\d版本期間持續開放</value>
</data> </data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve"> <data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活動時間〓|祈願時間|【上架時間】|〓折扣時間〓).*?(\d\.\d).*?版本更新後.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>(?:〓活動時間〓|祈願時間|【上架時間】|〓折扣時間〓).*?(\d\.\d版本更新後).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>親愛.*?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新維護預告</value>
</data> </data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve"> <data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value> <value>〓更新時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -1,46 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Abstraction;
namespace Snap.Hutao.Service.Abstraction;
internal static class AppDbServiceAppDbEntityExtension
{
public static int DeleteByInnerId<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class, IAppDbEntity
{
return service.DeleteByInnerId(entity.InnerId);
}
public static int DeleteByInnerId<TEntity>(this IAppDbService<TEntity> service, Guid innerId)
where TEntity : class, IAppDbEntity
{
return service.Delete(e => e.InnerId == innerId);
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.DeleteByInnerIdAsync(entity.InnerId, token);
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, Guid innerId, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.DeleteAsync(e => e.InnerId == innerId, 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,53 +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.ListAsync(query => query.Where(predicate), token);
}
public static ValueTask<List<TResult>> ListAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, IQueryable<TResult>> query, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query1, token) => query(query1).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

@@ -1,176 +0,0 @@
// 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.Database;
using System.Linq.Expressions;
namespace Snap.Hutao.Service.Abstraction;
internal static class AppDbServiceExtension
{
public static TResult Execute<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, TResult> func)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return func(appDbContext.Set<TEntity>());
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, ValueTask<TResult>> asyncFunc)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>()).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, CancellationToken, ValueTask<TResult>> asyncFunc, CancellationToken token)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>(), token).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, Task<TResult>> asyncFunc)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>()).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, CancellationToken, Task<TResult>> asyncFunc, CancellationToken token)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>(), token).ConfigureAwait(false);
}
}
public static int Add<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
return service.Execute(dbset => dbset.AddAndSave(entity));
}
public static ValueTask<int> AddAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.AddAndSaveAsync(entity, token), token);
}
public static int AddRange<TEntity>(this IAppDbService<TEntity> service, IEnumerable<TEntity> entities)
where TEntity : class
{
return service.Execute(dbset => dbset.AddRangeAndSave(entities));
}
public static ValueTask<int> AddRangeAsync<TEntity>(this IAppDbService<TEntity> service, IEnumerable<TEntity> entities, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.AddRangeAndSaveAsync(entities, token), token);
}
public static TResult Query<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, TResult> func)
where TEntity : class
{
return service.Execute(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, ValueTask<TResult>> func)
where TEntity : class
{
return service.ExecuteAsync(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, CancellationToken, ValueTask<TResult>> func, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => func(dbset.AsNoTracking(), token), token);
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, Task<TResult>> func)
where TEntity : class
{
return service.ExecuteAsync(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, CancellationToken, Task<TResult>> func, CancellationToken token = default)
where TEntity : class
{
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 TEntity? SingleOrDefault<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.SingleOrDefault(predicate));
}
public static ValueTask<TEntity?> SingleOrDefaultAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.SingleOrDefaultAsync(predicate, token), token);
}
public static int Update<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
return service.Execute(dbset => dbset.UpdateAndSave(entity));
}
public static ValueTask<int> UpdateAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.UpdateAndSaveAsync(entity, token), token);
}
public static int Delete<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
return service.Execute(dbset => dbset.RemoveAndSave(entity));
}
public static int Delete<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Execute(dbset => dbset.Where(predicate).ExecuteDelete());
}
public static ValueTask<int> DeleteAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.RemoveAndSaveAsync(entity, token), token);
}
public static ValueTask<int> DeleteAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.Where(predicate).ExecuteDeleteAsync(token), token);
}
}

View File

@@ -14,10 +14,20 @@ namespace Snap.Hutao.Service.Abstraction;
/// 数据库存储选项的设置 /// 数据库存储选项的设置
/// </summary> /// </summary>
[ConstructorGenerated] [ConstructorGenerated]
internal abstract partial class DbStoreOptions : ObservableObject internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbStoreOptions>
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
/// <inheritdoc/>
public DbStoreOptions Value { get => this; }
/// <summary>
/// 从数据库中获取字符串数据
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
protected string GetOption(ref string? storage, string key, string defaultValue = "") protected string GetOption(ref string? storage, string key, string defaultValue = "")
{ {
return GetOption(ref storage, key, () => defaultValue); return GetOption(ref storage, key, () => defaultValue);
@@ -39,6 +49,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage; return storage;
} }
/// <summary>
/// 从数据库中获取bool数据
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
protected bool GetOption(ref bool? storage, string key, bool defaultValue = false) protected bool GetOption(ref bool? storage, string key, bool defaultValue = false)
{ {
return GetOption(ref storage, key, () => defaultValue); return GetOption(ref storage, key, () => defaultValue);
@@ -61,6 +78,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage.Value; return storage.Value;
} }
/// <summary>
/// 从数据库中获取int数据
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
protected int GetOption(ref int? storage, string key, int defaultValue = 0) protected int GetOption(ref int? storage, string key, int defaultValue = 0)
{ {
return GetOption(ref storage, key, () => defaultValue); return GetOption(ref storage, key, () => defaultValue);
@@ -83,6 +107,15 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage.Value; return storage.Value;
} }
/// <summary>
/// 从数据库中获取任何类型的数据
/// </summary>
/// <typeparam name="T">数据的类型</typeparam>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="deserializer">反序列化器</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
[return: NotNull] [return: NotNull]
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, [DisallowNull] T defaultValue) protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, [DisallowNull] T defaultValue)
{ {
@@ -127,6 +160,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage; return storage;
} }
/// <summary>
/// 将值存入数据库
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="propertyName">属性名称</param>
protected void SetOption(ref string? storage, string key, string? value, [CallerMemberName] string? propertyName = null) protected void SetOption(ref string? storage, string key, string? value, [CallerMemberName] string? propertyName = null)
{ {
if (!SetProperty(ref storage, value, propertyName)) if (!SetProperty(ref storage, value, propertyName))
@@ -142,6 +182,14 @@ internal abstract partial class DbStoreOptions : ObservableObject
} }
} }
/// <summary>
/// 将值存入数据库
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="propertyName">属性名称</param>
/// <returns>是否设置了值</returns>
protected bool SetOption(ref bool? storage, string key, bool value, [CallerMemberName] string? propertyName = null) protected bool SetOption(ref bool? storage, string key, bool value, [CallerMemberName] string? propertyName = null)
{ {
bool set = SetProperty(ref storage, value, propertyName); bool set = SetProperty(ref storage, value, propertyName);
@@ -160,6 +208,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return set; return set;
} }
/// <summary>
/// 将值存入数据库
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="propertyName">属性名称</param>
protected void SetOption(ref int? storage, string key, int value, [CallerMemberName] string? propertyName = null) protected void SetOption(ref int? storage, string key, int value, [CallerMemberName] string? propertyName = null)
{ {
if (!SetProperty(ref storage, value, propertyName)) if (!SetProperty(ref storage, value, propertyName))
@@ -175,6 +230,15 @@ internal abstract partial class DbStoreOptions : ObservableObject
} }
} }
/// <summary>
/// 将值存入数据库
/// </summary>
/// <typeparam name="T">数据的类型</typeparam>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="serializer">序列化器</param>
/// <param name="propertyName">属性名称</param>
protected void SetOption<T>(ref T? storage, string key, T value, Func<T, string> serializer, [CallerMemberName] string? propertyName = null) protected void SetOption<T>(ref T? storage, string key, T value, Func<T, string> serializer, [CallerMemberName] string? propertyName = null)
{ {
if (!SetProperty(ref storage, value, propertyName)) if (!SetProperty(ref storage, value, propertyName))

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
namespace Snap.Hutao.Service.Abstraction;
/// <summary>
/// 公告服务
/// </summary>
[HighQuality]
internal interface IAnnouncementService
{
/// <summary>
/// 异步获取游戏公告与活动,通常会进行缓存
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>公告包装器</returns>
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default);
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
internal interface IAppDbService<TEntity> : IAppInfrastructureService
where TEntity : class
{
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
internal interface IAppInfrastructureService
{
IServiceProvider ServiceProvider { get; }
}

View File

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

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Service.Abstraction;
internal static class ServiceScopeExtension
{
public static TService GetRequiredService<TService>(this IServiceScope scope)
where TService : class
{
return scope.ServiceProvider.GetRequiredService<TService>();
}
public static TDbContext GetDbContext<TDbContext>(this IServiceScope scope)
where TDbContext : DbContext
{
return scope.GetRequiredService<TDbContext>();
}
public static AppDbContext GetAppDbContext(this IServiceScope scope)
{
return scope.GetDbContext<AppDbContext>();
}
}

View File

@@ -2,11 +2,9 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Collection;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.Achievement; using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Service.Abstraction;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement; namespace Snap.Hutao.Service.Achievement;
@@ -23,52 +21,73 @@ internal sealed partial class AchievementDbBulkOperation
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly ILogger<AchievementDbBulkOperation> logger; private readonly ILogger<AchievementDbBulkOperation> logger;
/// <summary>
/// 合并
/// </summary>
/// <param name="archiveId">成就id</param>
/// <param name="items">待合并的项</param>
/// <param name="aggressive">是否贪婪</param>
/// <returns>导入结果</returns>
public ImportResult Merge(Guid archiveId, IEnumerable<UIAFItem> items, bool aggressive) public ImportResult Merge(Guid archiveId, IEnumerable<UIAFItem> items, bool aggressive)
{ {
logger.LogInformation("Perform merge operation for [Archive: {Id}], [Aggressive: {Aggressive}]", archiveId, aggressive); logger.LogInformation("Perform {Method} Operation for archive: {Id}, Aggressive: {Aggressive}", nameof(Merge), archiveId, aggressive);
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {
AppDbContext appDbContext = scope.GetAppDbContext(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
.AsNoTracking() .AsNoTracking()
.Where(a => a.ArchiveId == archiveId) .Where(a => a.ArchiveId == archiveId)
.OrderBy(a => a.Id); .OrderBy(a => a.Id);
(int add, int update) = (0, 0); int add = 0;
int update = 0;
using (TwoEnumerbleEnumerator<EntityAchievement, UIAFItem> enumerator = new(oldData, items)) using (IEnumerator<EntityAchievement> entityEnumerator = oldData.GetEnumerator())
{ {
(bool moveEntity, bool moveUIAF) = (true, true); using (IEnumerator<UIAFItem> uiafEnumerator = items.GetEnumerator())
{
bool moveEntity = true;
bool moveUIAF = true;
while (true) while (true)
{ {
if (!enumerator.MoveNext(ref moveEntity, ref moveUIAF)) bool moveEntityResult = moveEntity && entityEnumerator.MoveNext();
bool moveUIAFResult = moveUIAF && uiafEnumerator.MoveNext();
if (!(moveEntityResult || moveUIAFResult))
{ {
break; break;
} }
else
(EntityAchievement? entity, UIAFItem? uiaf) = enumerator.Current; {
EntityAchievement? entity = entityEnumerator.Current;
switch (entity, uiaf) UIAFItem? uiaf = uiafEnumerator.Current;
if (entity is null && uiaf is not null)
{ {
case (null, not null):
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf)); appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
add++; add++;
continue; continue;
case (not null, null): }
continue; // Skipped else if (entity is not null && uiaf is null)
default: {
// skip
continue;
}
ArgumentNullException.ThrowIfNull(entity); ArgumentNullException.ThrowIfNull(entity);
ArgumentNullException.ThrowIfNull(uiaf); ArgumentNullException.ThrowIfNull(uiaf);
switch (entity.Id.CompareTo(uiaf.Id)) if (entity.Id < uiaf.Id)
{ {
case < 0: moveEntity = true;
(moveEntity, moveUIAF) = (true, false); moveUIAF = false;
break; }
case 0: else if (entity.Id == uiaf.Id)
(moveEntity, moveUIAF) = (true, true); {
moveEntity = true;
moveUIAF = true;
if (aggressive) if (aggressive)
{ {
@@ -76,78 +95,96 @@ internal sealed partial class AchievementDbBulkOperation
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf)); appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
update++; update++;
} }
}
break; else
case > 0: {
(moveEntity, moveUIAF) = (false, true); // entity.Id > uiaf.Id
moveEntity = false;
moveUIAF = true;
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf)); appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
add++; add++;
break;
} }
}
break;
} }
} }
} }
logger.LogInformation("Merge operation complete, [Add: {Add}], [Update: {Update}]", add, update); logger.LogInformation("{Method} Operation Complete, Add: {Add}, Update: {Update}", nameof(Merge), add, update);
return new(add, update, 0); return new(add, update, 0);
} }
} }
/// <summary>
/// 覆盖
/// </summary>
/// <param name="archiveId">成就id</param>
/// <param name="items">待覆盖的项</param>
/// <returns>导入结果</returns>
public ImportResult Overwrite(Guid archiveId, IEnumerable<EntityAchievement> items) public ImportResult Overwrite(Guid archiveId, IEnumerable<EntityAchievement> items)
{ {
logger.LogInformation("Perform Overwrite Operation for [Archive: {Id}]", archiveId); logger.LogInformation("Perform {Method} Operation for archive: {Id}", nameof(Overwrite), archiveId);
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {
AppDbContext appDbContext = scope.GetAppDbContext(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
.AsNoTracking() .AsNoTracking()
.Where(a => a.ArchiveId == archiveId) .Where(a => a.ArchiveId == archiveId)
.OrderBy(a => a.Id); .OrderBy(a => a.Id);
(int add, int update, int remove) = (0, 0, 0); int add = 0;
int update = 0;
int remove = 0;
using (TwoEnumerbleEnumerator<EntityAchievement, EntityAchievement> enumerator = new(oldData, items)) using (IEnumerator<EntityAchievement> oldDataEnumerator = oldData.GetEnumerator())
{ {
(bool moveOld, bool moveNew) = (true, true); using (IEnumerator<EntityAchievement> newDataEnumerator = items.GetEnumerator())
{
bool moveOld = true;
bool moveNew = true;
while (true) while (true)
{ {
if (!enumerator.MoveNext(ref moveOld, ref moveNew)) bool moveOldResult = moveOld && oldDataEnumerator.MoveNext();
{ bool moveNewResult = moveNew && newDataEnumerator.MoveNext();
break;
}
(EntityAchievement? oldEntity, EntityAchievement? newEntity) = enumerator.Current; if (moveOldResult || moveNewResult)
{
switch (oldEntity, newEntity) EntityAchievement? oldEntity = oldDataEnumerator.Current;
EntityAchievement? newEntity = newDataEnumerator.Current;
if (oldEntity is null && newEntity is not null)
{ {
case (null, not null):
appDbContext.Achievements.AddAndSave(newEntity); appDbContext.Achievements.AddAndSave(newEntity);
add++; add++;
continue; continue;
case (not null, null): }
else if (oldEntity is not null && newEntity is null)
{
appDbContext.Achievements.RemoveAndSave(oldEntity); appDbContext.Achievements.RemoveAndSave(oldEntity);
remove++; remove++;
continue; continue;
default: }
ArgumentNullException.ThrowIfNull(oldEntity); ArgumentNullException.ThrowIfNull(oldEntity);
ArgumentNullException.ThrowIfNull(newEntity); ArgumentNullException.ThrowIfNull(newEntity);
switch (oldEntity.Id.CompareTo(newEntity.Id)) if (oldEntity.Id < newEntity.Id)
{ {
case < 0: moveOld = true;
(moveOld, moveNew) = (true, false); moveNew = false;
break; appDbContext.Achievements.RemoveAndSave(oldEntity);
case 0: remove++;
(moveOld, moveNew) = (true, true); }
else if (oldEntity.Id == newEntity.Id)
{
moveOld = true;
moveNew = true;
if (oldEntity.Equals(newEntity)) if (oldEntity.Equals(newEntity))
{ {
// Skip same entry, reduce write operation. // skip same entry.
continue; continue;
} }
else else
@@ -156,22 +193,25 @@ internal sealed partial class AchievementDbBulkOperation
appDbContext.Achievements.AddAndSave(newEntity); appDbContext.Achievements.AddAndSave(newEntity);
update++; update++;
} }
}
break; else
case > 0: {
(moveOld, moveNew) = (false, true); // entity.Id > uiaf.Id
moveOld = false;
moveNew = true;
appDbContext.Achievements.AddAndSave(newEntity); appDbContext.Achievements.AddAndSave(newEntity);
add++; add++;
break;
} }
}
else
{
break; break;
} }
} }
} }
}
logger.LogInformation("Overwrite Operation Complete, Add: {Add}, Update: {Update}, Remove: {Remove}", add, update, remove); logger.LogInformation("{Method} Operation Complete, Add: {Add}, Update: {Update}, Remove: {Remove}", nameof(Overwrite), add, update, remove);
return new(add, update, remove); return new(add, update, remove);
} }
} }

View File

@@ -2,105 +2,167 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Abstraction;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement; namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 成就数据库服务
/// </summary>
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IAchievementDbService))] [Injection(InjectAs.Singleton, typeof(IAchievementDbService))]
internal sealed partial class AchievementDbService : IAchievementDbService internal sealed partial class AchievementDbService : IAchievementDbService
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId) public Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId)
{ {
Dictionary<AchievementId, EntityAchievement> entities;
try try
{ {
return this.Query<EntityAchievement, Dictionary<AchievementId, EntityAchievement>>(query => query using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
entities = appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId) .Where(a => a.ArchiveId == archiveId)
.ToDictionary(a => (AchievementId)a.Id)); .ToDictionary(a => (AchievementId)a.Id);
}
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
throw HutaoException.UserdataCorrupted(SH.ServiceAchievementUserdataCorruptedAchievementIdNotUnique, ex); throw ThrowHelper.DatabaseCorrupted(SH.ServiceAchievementUserdataCorruptedInnerIdNotUnique, ex);
}
} }
public ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default) return entities;
}
public async ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId)
{ {
return this.QueryAsync<EntityAchievement, int>( using (IServiceScope scope = serviceProvider.CreateScope())
(query, token) => query {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId) .Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED) .Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.CountAsync(token), .CountAsync()
token); .ConfigureAwait(false);
}
} }
[SuppressMessage("", "CA1305")] [SuppressMessage("", "CA1305")]
public ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default) public async ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take)
{ {
return this.ListAsync<EntityAchievement, EntityAchievement>( using (IServiceScope scope = serviceProvider.CreateScope())
query => query {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId) .Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED) .Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time.ToString()) .OrderByDescending(a => a.Time.ToString())
.Take(take), .Take(take)
token); .ToListAsync()
.ConfigureAwait(false);
}
} }
public void OverwriteAchievement(EntityAchievement achievement) public void OverwriteAchievement(EntityAchievement achievement)
{ {
this.DeleteByInnerId(achievement); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Delete exists one.
appDbContext.Achievements.ExecuteDeleteWhere(e => e.InnerId == achievement.InnerId);
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED) if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
{ {
this.Add(achievement); appDbContext.Achievements.AddAndSave(achievement);
}
} }
} }
public async ValueTask OverwriteAchievementAsync(EntityAchievement achievement, CancellationToken token = default) public async ValueTask OverwriteAchievementAsync(EntityAchievement achievement)
{ {
await this.DeleteByInnerIdAsync(achievement, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Delete exists one.
await appDbContext.Achievements.ExecuteDeleteWhereAsync(e => e.InnerId == achievement.InnerId).ConfigureAwait(false);
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED) if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
{ {
await this.AddAsync(achievement, token).ConfigureAwait(false); await appDbContext.Achievements.AddAndSaveAsync(achievement).ConfigureAwait(false);
}
} }
} }
public ObservableCollection<AchievementArchive> GetAchievementArchiveCollection() public ObservableCollection<AchievementArchive> GetAchievementArchiveCollection()
{ {
return this.ObservableCollection<AchievementArchive>(); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
}
} }
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive, CancellationToken token = default) public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive)
{ {
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// It will cascade deleted the achievements. // It will cascade deleted the achievements.
await this.DeleteAsync(archive, token).ConfigureAwait(false); await appDbContext.AchievementArchives.RemoveAndSaveAsync(archive).ConfigureAwait(false);
}
} }
public List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId) public List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId)
{ {
return this.ListByArchiveId<EntityAchievement>(archiveId); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<EntityAchievement> result = appDbContext.Achievements.AsNoTracking().Where(i => i.ArchiveId == archiveId);
return [.. result];
}
} }
public ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default) public async ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId)
{ {
return this.ListByArchiveIdAsync<EntityAchievement>(archiveId, token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Achievements
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.ToListAsync()
.ConfigureAwait(false);
}
} }
public List<AchievementArchive> GetAchievementArchiveList() public List<AchievementArchive> GetAchievementArchiveList()
{ {
return this.List<AchievementArchive>(); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<AchievementArchive> result = appDbContext.AchievementArchives.AsNoTracking();
return [.. result];
}
} }
public ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default) public async ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync()
{ {
return this.ListAsync<AchievementArchive>(token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.AchievementArchives.AsNoTracking().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;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Achievement; using Snap.Hutao.ViewModel.Achievement;
using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement;
namespace Snap.Hutao.Service.Achievement; namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 成就服务
/// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAchievementService))] [Injection(InjectAs.Scoped, typeof(IAchievementService))]
@@ -24,127 +25,21 @@ internal sealed partial class AchievementService : IAchievementService
private readonly RuntimeOptions runtimeOptions; private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private ObservableCollection<AchievementArchive>? archiveCollection; /// <inheritdoc/>
public List<AchievementView> GetAchievementViewList(AchievementArchive archive, List<MetadataAchievement> metadata)
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)
{ {
Dictionary<AchievementId, EntityAchievement> entities = achievementDbService.GetAchievementMapByArchiveId(archive.InnerId); 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); EntityAchievement entity = entities.GetValueOrDefault(meta.Id) ?? EntityAchievement.From(archive.InnerId, meta.Id);
return new AchievementView(entity, meta); return new AchievementView(entity, meta);
}); });
} }
/// <inheritdoc/>
public void SaveAchievement(AchievementView achievement) public void SaveAchievement(AchievementView achievement)
{ {
achievementDbService.OverwriteAchievement(achievement.Entity); 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))] [Injection(InjectAs.Scoped, typeof(IAchievementStatisticsService))]
internal sealed partial class AchievementStatisticsService : IAchievementStatisticsService internal sealed partial class AchievementStatisticsService : IAchievementStatisticsService
{ {
private const int AchievementCardTakeCount = 2;
private readonly IAchievementDbService achievementDbService; private readonly IAchievementDbService achievementDbService;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
/// <inheritdoc/> /// <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(); await taskContext.SwitchToBackgroundAsync();
List<AchievementStatistics> results = []; 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 int finishedCount = await achievementDbService
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId, token) .GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false); .ConfigureAwait(false);
int totalCount = context.IdAchievementMap.Count; int totalCount = achievementMap.Count;
List<EntityAchievement> achievements = await achievementDbService List<EntityAchievement> achievements = await achievementDbService
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, AchievementCardTakeCount, token) .GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, 2)
.ConfigureAwait(false); .ConfigureAwait(false);
results.Add(new() results.Add(new()
{ {
DisplayName = archive.Name, DisplayName = archive.Name,
FinishDescription = AchievementStatistics.Format(finishedCount, totalCount, out _), 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> /// </summary>
[HighQuality] [HighQuality]
internal enum ArchiveAddResultKind internal enum ArchiveAddResult
{ {
/// <summary> /// <summary>
/// 添加成功 /// 添加成功

View File

@@ -2,33 +2,32 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Abstraction;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement; namespace Snap.Hutao.Service.Achievement;
internal interface IAchievementDbService : IAppDbService<Model.Entity.AchievementArchive>, IAppDbService<EntityAchievement> internal interface IAchievementDbService
{ {
ValueTask RemoveAchievementArchiveAsync(Model.Entity.AchievementArchive archive, CancellationToken token = default); ValueTask RemoveAchievementArchiveAsync(Model.Entity.AchievementArchive archive);
ObservableCollection<Model.Entity.AchievementArchive> GetAchievementArchiveCollection(); ObservableCollection<Model.Entity.AchievementArchive> GetAchievementArchiveCollection();
List<Model.Entity.AchievementArchive> GetAchievementArchiveList(); List<Model.Entity.AchievementArchive> GetAchievementArchiveList();
ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default); ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync();
List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId); List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId);
ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default); ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId);
Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId); Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId);
ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default); ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId);
ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default); ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take);
void OverwriteAchievement(EntityAchievement achievement); void OverwriteAchievement(EntityAchievement achievement);
ValueTask OverwriteAchievementAsync(EntityAchievement achievement, CancellationToken token = default); ValueTask OverwriteAchievementAsync(EntityAchievement achievement);
} }

View File

@@ -32,7 +32,13 @@ internal interface IAchievementService
/// <returns>UIAF</returns> /// <returns>UIAF</returns>
ValueTask<UIAF> ExportToUIAFAsync(EntityArchive selectedArchive); 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> /// <summary>
/// 异步导入UIAF数据 /// 异步导入UIAF数据
@@ -41,7 +47,7 @@ internal interface IAchievementService
/// <param name="list">UIAF数据</param> /// <param name="list">UIAF数据</param>
/// <param name="strategy">选项</param> /// <param name="strategy">选项</param>
/// <returns>导入结果</returns> /// <returns>导入结果</returns>
ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategyKind strategy); ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy);
/// <summary> /// <summary>
/// 异步移除存档 /// 异步移除存档
@@ -61,5 +67,5 @@ internal interface IAchievementService
/// </summary> /// </summary>
/// <param name="newArchive">新存档</param> /// <param name="newArchive">新存档</param>
/// <returns>存档添加结果</returns> /// <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 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> /// </summary>
[HighQuality] [HighQuality]
internal enum ImportStrategyKind internal enum ImportStrategy
{ {
/// <summary> /// <summary>
/// 贪婪合并 /// 贪婪合并

View File

@@ -1,213 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core;
using Snap.Hutao.Service.Announcement;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Snap.Hutao.Web.Response;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using WebAnnouncement = Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement.Announcement;
namespace Snap.Hutao.Service;
/// <inheritdoc/>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAnnouncementService))]
internal sealed partial class AnnouncementService : IAnnouncementService
{
private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ITaskContext taskContext;
private readonly IMemoryCache memoryCache;
public async ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default)
{
// 缓存中存在记录,直接返回
if (memoryCache.TryGetRequiredValue($"{CacheKey}.{languageCode}.{region}", out AnnouncementWrapper? cache))
{
return cache;
}
await taskContext.SwitchToBackgroundAsync();
List<AnnouncementContent>? contents;
AnnouncementWrapper wrapper;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
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;
}
Dictionary<int, string> contentMap = contents.ToDictionary(id => id.AnnId, content => content.Content);
// 将活动公告置于前方
wrapper.List.Reverse();
PreprocessAnnouncements(contentMap, wrapper.List, new(wrapper.TimeZone, 0, 0));
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
}
private static void PreprocessAnnouncements(Dictionary<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 将公告内容联入公告列表
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
item.Content = contentMap.GetValueOrDefault(item.AnnId, string.Empty);
}
}
AdjustAnnouncementTime(announcementListWrappers, offset);
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
item.Subtitle = new StringBuilder(item.Subtitle)
.Replace("\r<br>", string.Empty)
.Replace("<br />", string.Empty)
.ToString();
item.Content = AnnouncementRegex
.XmlTimeTagRegex()
.Replace(item.Content, x => x.Groups[1].Value);
}
}
}
private static void AdjustAnnouncementTime(List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 活动公告
List<WebAnnouncement> activities = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 1)
.List;
// 游戏公告
List<WebAnnouncement> announcements = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 2)
.List;
Dictionary<string, DateTimeOffset> versionStartTimes = [];
// 更新公告
if (announcements.SingleOrDefault(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title)) is { } versionUpdate)
{
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionUpdateMatch)
{
return;
}
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionUpdateMatch.Groups[1].ValueSpan, offset);
versionStartTimes.TryAdd(VersionRegex().Match(versionUpdate.Title).Groups[1].Value, versionUpdateTime);
}
// 更新预告
if (announcements.SingleOrDefault(ann => AnnouncementRegex.VersionUpdatePreviewTitleRegex.IsMatch(ann.Title)) is { } versionUpdatePreview)
{
if (AnnouncementRegex.VersionUpdatePreviewTimeRegex.Match(versionUpdatePreview.Content) is not { Success: true } versionUpdatePreviewMatch)
{
return;
}
DateTimeOffset versionUpdatePreviewTime = UnsafeDateTimeOffset.ParseDateTime(versionUpdatePreviewMatch.Groups[1].ValueSpan, offset);
versionStartTimes.TryAdd(VersionRegex().Match(versionUpdatePreview.Title).Groups[1].Value, versionUpdatePreviewTime);
}
foreach (ref readonly WebAnnouncement announcement in CollectionsMarshal.AsSpan(activities))
{
DateTimeOffset versionStartTime;
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
{
if (versionStartTimes.TryGetValue(permanent.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
continue;
}
}
if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent)
{
if (versionStartTimes.TryGetValue(persistent.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
announcement.EndTime = versionStartTime + TimeSpan.FromDays(42);
continue;
}
}
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
{
if (versionStartTimes.TryGetValue(transient.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
}
}
MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex().Matches(announcement.Content);
if (matches.Count < 2)
{
continue;
}
List<DateTimeOffset> dateTimes = [];
foreach (Match timeMatch in (IList<Match>)matches)
{
dateTimes.Add(UnsafeDateTimeOffset.ParseDateTime(timeMatch.Groups[1].ValueSpan, offset));
}
DateTimeOffset min = DateTimeOffset.MaxValue;
DateTimeOffset max = DateTimeOffset.MinValue;
foreach (DateTimeOffset time in dateTimes)
{
if (time < min)
{
min = time;
}
if (time > max)
{
max = time;
}
}
announcement.StartTime = min;
announcement.EndTime = max;
}
}
[GeneratedRegex("(\\d\\.\\d)")]
private static partial Regex VersionRegex();
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
namespace Snap.Hutao.Service.Announcement;
/// <summary>
/// 公告服务
/// </summary>
[HighQuality]
internal interface IAnnouncementService
{
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken token = default);
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Service;
/// <inheritdoc/>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IAnnouncementService))]
internal sealed partial class AnnouncementService : IAnnouncementService
{
private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
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)
{
// 缓存中存在记录,直接返回
if (memoryCache.TryGetRequiredValue($"{CacheKey}.{languageCode}.{region}", out AnnouncementWrapper? cache))
{
return cache;
}
await taskContext.SwitchToBackgroundAsync();
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
.GetAnnouncementsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementWrapperResponse.IsOk())
{
return default!;
}
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();
PreprocessAnnouncements(contentMap, wrapper.List, new(wrapper.TimeZone, 0, 0));
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
}
private static void PreprocessAnnouncements(Dictionary<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 将公告内容联入公告列表
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
contentMap.TryGetValue(item.AnnId, out string? rawContent);
item.Content = rawContent ?? string.Empty;
}
}
AdjustAnnouncementTime(announcementListWrappers, offset);
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
item.Subtitle = new StringBuilder(item.Subtitle)
.Replace("\r<br>", string.Empty)
.Replace("<br />", string.Empty)
.ToString();
item.Content = AnnouncementRegex
.XmlTimeTagRegex()
.Replace(item.Content, x => x.Groups[1].Value);
}
}
}
private static void AdjustAnnouncementTime(List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 活动公告
List<Announcement> activities = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 1)
.List;
// 更新公告
Announcement versionUpdate = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 2)
.List
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionMatch)
{
return;
}
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionMatch.Groups[1].ValueSpan, offset);
foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities))
{
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
{
announcement.StartTime = versionUpdateTime;
continue;
}
if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent)
{
announcement.StartTime = versionUpdateTime;
announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42);
continue;
}
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
{
announcement.StartTime = versionUpdateTime;
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
}
MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex().Matches(announcement.Content);
if (matches.Count < 2)
{
continue;
}
List<DateTimeOffset> dateTimes = [];
foreach (Match timeMatch in (IList<Match>)matches)
{
dateTimes.Add(UnsafeDateTimeOffset.ParseDateTime(timeMatch.Groups[1].ValueSpan, offset));
}
DateTimeOffset min = DateTimeOffset.MaxValue;
DateTimeOffset max = DateTimeOffset.MinValue;
foreach (DateTimeOffset time in dateTimes)
{
if (time < min)
{
min = time;
}
if (time > max)
{
max = time;
}
}
announcement.StartTime = min;
announcement.EndTime = max;
}
}
}

View File

@@ -16,7 +16,6 @@ namespace Snap.Hutao.Service;
internal sealed partial class AppOptions : DbStoreOptions internal sealed partial class AppOptions : DbStoreOptions
{ {
private bool? isEmptyHistoryWishVisible; private bool? isEmptyHistoryWishVisible;
private bool? isUnobtainedWishItemVisible;
private BackdropType? backdropType; private BackdropType? backdropType;
private ElementTheme? elementTheme; private ElementTheme? elementTheme;
private BackgroundImageType? backgroundImageType; private BackgroundImageType? backgroundImageType;
@@ -25,16 +24,10 @@ internal sealed partial class AppOptions : DbStoreOptions
public bool IsEmptyHistoryWishVisible public bool IsEmptyHistoryWishVisible
{ {
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, false); get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value); set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
} }
public bool IsUnobtainedWishItemVisible
{
get => GetOption(ref isUnobtainedWishItemVisible, SettingEntry.IsUnobtainedWishItemVisible, false);
set => SetOption(ref isUnobtainedWishItemVisible, SettingEntry.IsUnobtainedWishItemVisible, value);
}
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0); public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0);
public BackdropType BackdropType public BackdropType BackdropType

View File

@@ -12,9 +12,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using System.Reflection.Emit;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar; using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
@@ -23,6 +21,9 @@ using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
namespace Snap.Hutao.Service.AvatarInfo; namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息数据库操作
/// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Singleton)] [Injection(InjectAs.Singleton)]
@@ -31,10 +32,18 @@ internal sealed partial class AvatarInfoDbBulkOperation
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly IAvatarInfoDbService avatarInfoDbService; private readonly IAvatarInfoDbService avatarInfoDbService;
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByShowcaseAsync(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token) /// <summary>
/// 更新数据库角色信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="webInfos">Enka信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public List<EntityAvatarInfo> UpdateDbAvatarInfosByShowcase(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{ {
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false); token.ThrowIfCancellationRequested();
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap); List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {
@@ -47,19 +56,28 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue; continue;
} }
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(webInfo.AvatarId); token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo); AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo);
} }
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false); token.ThrowIfCancellationRequested();
return avatarInfoDbService.GetAvatarInfoListByUid(uid);
} }
} }
/// <summary>
/// 米游社我的角色方式 更新数据库角色信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token) public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
{ {
token.ThrowIfCancellationRequested();
string uid = userAndUid.Uid.Value; string uid = userAndUid.Uid.Value;
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false); List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap); EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {
@@ -72,20 +90,16 @@ internal sealed partial class AvatarInfoDbBulkOperation
.GetPlayerInfoAsync(userAndUid, token) .GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false); .ConfigureAwait(false);
if (!playerInfoResponse.IsOk()) if (playerInfoResponse.IsOk())
{ {
goto Return;
}
Response<CharacterWrapper> charactersResponse = await gameRecordClient Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token) .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false); .ConfigureAwait(false);
if (!charactersResponse.IsOk()) token.ThrowIfCancellationRequested();
{
goto Return;
}
if (charactersResponse.IsOk())
{
List<RecordCharacter> characters = charactersResponse.Data.Avatars; List<RecordCharacter> characters = charactersResponse.Data.Avatars;
GameRecordCharacterAvatarInfoTransformer transformer = serviceProvider GameRecordCharacterAvatarInfoTransformer transformer = serviceProvider
@@ -98,21 +112,29 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue; continue;
} }
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(character.Id); token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
AddOrUpdateAvatarInfo(entity, character.Id, uid, appDbContext, transformer, character); AddOrUpdateAvatarInfo(entity, character.Id, uid, appDbContext, transformer, character);
} }
} }
}
}
Return:
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false); return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
} }
/// <summary>
/// 米游社养成计算方式 更新数据库角色信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token) public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
string uid = userAndUid.Uid.Value; string uid = userAndUid.Uid.Value;
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false); List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap); EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())
{ {
@@ -133,6 +155,8 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue; continue;
} }
token.ThrowIfCancellationRequested();
Response<AvatarDetail> detailAvatarResponse = await calculateClient Response<AvatarDetail> detailAvatarResponse = await calculateClient
.GetAvatarDetailAsync(userAndUid, avatar, token) .GetAvatarDetailAsync(userAndUid, avatar, token)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -142,7 +166,8 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue; continue;
} }
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(avatar.Id); token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
AddOrUpdateAvatarInfo(entity, avatar.Id, uid, appDbContext, transformer, detailAvatarResponse.Data); AddOrUpdateAvatarInfo(entity, avatar.Id, uid, appDbContext, transformer, detailAvatarResponse.Data);
} }
} }
@@ -156,15 +181,16 @@ internal sealed partial class AvatarInfoDbBulkOperation
if (entity is null) if (entity is null)
{ {
entity = EntityAvatarInfo.From(uid, webInfo); entity = EntityAvatarInfo.From(uid, webInfo);
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
} }
else else
{ {
entity.Info = webInfo; entity.Info = webInfo;
}
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow; entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity); appDbContext.AvatarInfos.UpdateAndSave(entity);
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddOrUpdateAvatarInfo(EntityAvatarInfo? entity, in AvatarId avatarId, string uid, AppDbContext appDbContext, CalculateAvatarDetailAvatarInfoTransformer transformer, AvatarDetail source) private static void AddOrUpdateAvatarInfo(EntityAvatarInfo? entity, in AvatarId avatarId, string uid, AppDbContext appDbContext, CalculateAvatarDetailAvatarInfoTransformer transformer, AvatarDetail source)
@@ -174,17 +200,18 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId }; EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source); transformer.Transform(ref avatarInfo, source);
entity = EntityAvatarInfo.From(uid, avatarInfo); entity = EntityAvatarInfo.From(uid, avatarInfo);
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
} }
else else
{ {
EnkaAvatarInfo avatarInfo = entity.Info; EnkaAvatarInfo avatarInfo = entity.Info;
transformer.Transform(ref avatarInfo, source); transformer.Transform(ref avatarInfo, source);
entity.Info = avatarInfo; entity.Info = avatarInfo;
}
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow; entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity); appDbContext.AvatarInfos.UpdateAndSave(entity);
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddOrUpdateAvatarInfo(EntityAvatarInfo? entity, in AvatarId avatarId, string uid, AppDbContext appDbContext, GameRecordCharacterAvatarInfoTransformer transformer, Character source) private static void AddOrUpdateAvatarInfo(EntityAvatarInfo? entity, in AvatarId avatarId, string uid, AppDbContext appDbContext, GameRecordCharacterAvatarInfoTransformer transformer, Character source)
@@ -194,29 +221,29 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId }; EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source); transformer.Transform(ref avatarInfo, source);
entity = EntityAvatarInfo.From(uid, avatarInfo); entity = EntityAvatarInfo.From(uid, avatarInfo);
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
} }
else else
{ {
EnkaAvatarInfo avatarInfo = entity.Info; EnkaAvatarInfo avatarInfo = entity.Info;
transformer.Transform(ref avatarInfo, source); transformer.Transform(ref avatarInfo, source);
entity.Info = avatarInfo; entity.Info = avatarInfo;
}
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow; entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity); appDbContext.AvatarInfos.UpdateAndSave(entity);
} }
}
private void EnsureItemsAvatarIdUnique(ref List<EntityAvatarInfo> dbInfos, string uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap) private void EnsureItemsAvatarIdDistinct(ref List<EntityAvatarInfo> dbInfos, string uid)
{ {
dbInfoMap = []; int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count;
foreach (ref readonly EntityAvatarInfo info in CollectionsMarshal.AsSpan(dbInfos))
{ // Avatars are actually less than the list told us.
if (!dbInfoMap.TryAdd(info.Info.AvatarId, info)) // This means that there are duplicate items.
if (distinctCount < dbInfos.Count)
{ {
avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid); avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid);
dbInfoMap.Clear(); dbInfos = [];
dbInfos.Clear();
}
} }
} }
} }

View File

@@ -4,7 +4,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo; namespace Snap.Hutao.Service.AvatarInfo;
@@ -15,25 +14,44 @@ internal sealed partial class AvatarInfoDbService : IAvatarInfoDbService
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid) public List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid)
{ {
return this.List(i => i.Uid == uid); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<EntityAvatarInfo> result = appDbContext.AvatarInfos.AsNoTracking().Where(i => i.Uid == uid);
return [.. result];
}
} }
public ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default) public async ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid)
{ {
return this.ListAsync(i => i.Uid == uid, token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.AvatarInfos
.AsNoTracking()
.Where(i => i.Uid == uid)
.ToListAsync()
.ConfigureAwait(false);
}
} }
public void RemoveAvatarInfoRangeByUid(string uid) public void RemoveAvatarInfoRangeByUid(string uid)
{ {
this.Delete(i => i.Uid == uid); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.AvatarInfos.ExecuteDeleteWhere(i => i.Uid == uid);
}
} }
public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default) public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid)
{ {
await this.DeleteAsync(i => i.Uid == uid, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.AvatarInfos.ExecuteDeleteWhereAsync(i => i.Uid == uid).ConfigureAwait(false);
}
} }
} }

View File

@@ -13,6 +13,9 @@ using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo; namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息服务
/// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))] [Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
@@ -20,77 +23,78 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
{ {
private readonly AvatarInfoDbBulkOperation avatarInfoDbBulkOperation; private readonly AvatarInfoDbBulkOperation avatarInfoDbBulkOperation;
private readonly IAvatarInfoDbService avatarInfoDbService; private readonly IAvatarInfoDbService avatarInfoDbService;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ILogger<AvatarInfoService> logger; private readonly ILogger<AvatarInfoService> logger;
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private readonly ISummaryFactory summaryFactory; private readonly ISummaryFactory summaryFactory;
private readonly EnkaClient enkaClient;
public async ValueTask<ValueResult<RefreshResultKind, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default) /// <inheritdoc/>
public async ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
{ {
if (!await metadataService.InitializeAsync().ConfigureAwait(false)) if (await metadataService.InitializeAsync().ConfigureAwait(false))
{ {
return new(RefreshResultKind.MetadataNotInitialized, null); token.ThrowIfCancellationRequested();
}
switch (refreshOption) switch (refreshOption)
{ {
case RefreshOption.RequestFromEnkaAPI: case RefreshOption.RequestFromEnkaAPI:
{ {
EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false); EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (resp is null) if (resp is null)
{ {
return new(RefreshResultKind.APIUnavailable, default); return new(RefreshResult.APIUnavailable, default);
} }
if (!string.IsNullOrEmpty(resp.Message)) if (!string.IsNullOrEmpty(resp.Message))
{ {
return new(RefreshResultKind.StatusCodeNotSucceed, new Summary { Message = resp.Message }); return new(RefreshResult.StatusCodeNotSucceed, new Summary { Message = resp.Message });
} }
if (!resp.IsValid) if (!resp.IsValid)
{ {
return new(RefreshResultKind.ShowcaseNotOpen, default); return new(RefreshResult.ShowcaseNotOpen, default);
} }
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByShowcaseAsync(userAndUid.Uid.Value, resp.AvatarInfoList, token).ConfigureAwait(false); List<EntityAvatarInfo> list = avatarInfoDbBulkOperation.UpdateDbAvatarInfosByShowcase(userAndUid.Uid.Value, resp.AvatarInfoList, token);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary); return new(RefreshResult.Ok, summary);
} }
case RefreshOption.RequestFromHoyolabGameRecord: case RefreshOption.RequestFromHoyolabGameRecord:
{ {
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false); List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary); return new(RefreshResult.Ok, summary);
} }
case RefreshOption.RequestFromHoyolabCalculate: case RefreshOption.RequestFromHoyolabCalculate:
{ {
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false); List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary); return new(RefreshResult.Ok, summary);
} }
default: default:
{ {
List<EntityAvatarInfo> list = await avatarInfoDbService.GetAvatarInfoListByUidAsync(userAndUid.Uid.Value, token).ConfigureAwait(false); List<EntityAvatarInfo> list = await avatarInfoDbService.GetAvatarInfoListByUidAsync(userAndUid.Uid.Value).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false); Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary.Avatars.Count == 0 ? null : summary); token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
} }
} }
} }
else
{
return new(RefreshResult.MetadataNotInitialized, null);
}
}
private async ValueTask<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default) 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) return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false); ?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
} }
}
private async ValueTask<Summary> GetSummaryCoreAsync(IEnumerable<EntityAvatarInfo> avatarInfos, CancellationToken token) private async ValueTask<Summary> GetSummaryCoreAsync(IEnumerable<EntityAvatarInfo> avatarInfos, CancellationToken token)
{ {

View File

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

View File

@@ -1,242 +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 SetCostumeIconOrDefault<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.View.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
}
else
{
builder.View.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.View.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.View.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.View.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.Configure(b => b.View.CritScore = critScore);
}
public static TBuilder SetElement<TBuilder>(this TBuilder builder, ElementType element)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.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.View.FetterLevel = value);
}
return builder;
}
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, uint level)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.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.View.GameRecordRefreshTimeFormat = gameRecordRefreshTimeFormat);
}
public static TBuilder SetId<TBuilder>(this TBuilder builder, AvatarId id)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.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.View.LevelNumber = value);
}
return builder;
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Name = name);
}
public static TBuilder SetNameCard<TBuilder>(this TBuilder builder, Uri nameCard)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.NameCard = nameCard);
}
public static TBuilder SetProperties<TBuilder>(this TBuilder builder, List<AvatarProperty> properties)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Properties = properties);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Quality = quality);
}
public static TBuilder SetReliquaries<TBuilder>(this TBuilder builder, List<ReliquaryView> reliquaries)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Reliquaries = reliquaries);
}
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.View.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.View.Skills = skills);
}
public static TBuilder SetWeapon<TBuilder>(this TBuilder builder, WeaponView? weapon)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Weapon = weapon);
}
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class EquipViewBuilderExtension
{
public static TBuilder SetLevel<TBuilder, T>(this TBuilder builder, string level)
where TBuilder : IEquipViewBuilder<T>
where T : EquipView
{
return builder.Configure(b => b.View.Level = level);
}
public static TBuilder SetQuality<TBuilder, T>(this TBuilder builder, QualityType quality)
where TBuilder : IEquipViewBuilder<T>
where T : EquipView
{
return builder.Configure(b => b.View.Quality = quality);
}
public static TBuilder SetMainProperty<TBuilder, T>(this TBuilder builder, NameValue<string> mainProperty)
where TBuilder : IEquipViewBuilder<T>
where T : EquipView
{
return builder.Configure(b => b.View.MainProperty = mainProperty);
}
}

View File

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

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IEquipViewBuilder<T> : INameIconDescriptionBuilder<T>
where T : EquipView
{
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface INameIconDescriptionBuilder<T> : IBuilder
where T : NameIconDescription
{
T View { get; }
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IReliquaryViewBuilder : IEquipViewBuilder<ReliquaryView>, IScoreAccess
{
}

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 interface IScoreAccess
{
float Score { get; set; }
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IWeaponViewBuilder : IEquipViewBuilder<WeaponView>
{
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class NameIconDescriptionBuilderExtension
{
public static TBuilder SetDescription<TBuilder, T>(this TBuilder builder, string description)
where TBuilder : INameIconDescriptionBuilder<T>
where T : NameIconDescription
{
return builder.Configure(b => b.View.Description = description);
}
public static TBuilder SetIcon<TBuilder, T>(this TBuilder builder, Uri icon)
where TBuilder : INameIconDescriptionBuilder<T>
where T : NameIconDescription
{
return builder.Configure(b => b.View.Icon = icon);
}
public static TBuilder SetName<TBuilder, T>(this TBuilder builder, string name)
where TBuilder : INameIconDescriptionBuilder<T>
where T : NameIconDescription
{
return builder.Configure(b => b.View.Name = name);
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal sealed class ReliquaryViewBuilder : IReliquaryViewBuilder
{
public ReliquaryView View { get; } = new();
public float Score { get => View.Score; set => View.Score = value; }
}

View File

@@ -1,66 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class ReliquaryViewBuilderExtension
{
public static TBuilder SetComposedSubProperties<TBuilder>(this TBuilder builder, List<ReliquaryComposedSubProperty> composedSubProperties)
where TBuilder : IReliquaryViewBuilder
{
return builder.Configure(b => b.View.ComposedSubProperties = composedSubProperties);
}
public static TBuilder SetDescription<TBuilder>(this TBuilder builder, string description)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetDescription<TBuilder, ReliquaryView>(description);
}
public static TBuilder SetIcon<TBuilder>(this TBuilder builder, Uri icon)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetIcon<TBuilder, ReliquaryView>(icon);
}
public static TBuilder SetLevel<TBuilder>(this TBuilder builder, string level)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetLevel<TBuilder, ReliquaryView>(level);
}
public static TBuilder SetMainProperty<TBuilder>(this TBuilder builder, NameValue<string> mainProperty)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetMainProperty<TBuilder, ReliquaryView>(mainProperty);
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetName<TBuilder, ReliquaryView>(name);
}
public static TBuilder SetPrimarySubProperties<TBuilder>(this TBuilder builder, List<ReliquarySubProperty> primarySubProperties)
where TBuilder : IReliquaryViewBuilder
{
return builder.Configure(b => b.View.PrimarySubProperties = primarySubProperties);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetQuality<TBuilder, ReliquaryView>(quality);
}
public static TBuilder SetSecondarySubProperties<TBuilder>(this TBuilder builder, List<ReliquarySubProperty> secondarySubProperties)
where TBuilder : IReliquaryViewBuilder
{
return builder.Configure(b => b.View.SecondarySubProperties = secondarySubProperties);
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Abstraction.Extension;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class ScoreAccessExtension
{
public static TBuilder SetScore<TBuilder>(this TBuilder builder, float score)
where TBuilder : IBuilder, IScoreAccess
{
return builder.Configure(b => b.Score = score);
}
}

View File

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

View File

@@ -1,99 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
using Snap.Hutao.Web.Enka.Model;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class WeaponViewBuilderExtension
{
public static TBuilder SetAffixLevelNumber<TBuilder>(this TBuilder builder, uint affixLevelNumber)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.AffixLevelNumber = affixLevelNumber);
}
public static TBuilder SetAffixDescription<TBuilder>(this TBuilder builder, string? affixDescription)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.AffixDescription = affixDescription ?? string.Empty);
}
public static TBuilder SetAffixName<TBuilder>(this TBuilder builder, string? affixName)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.AffixName = affixName ?? string.Empty);
}
public static TBuilder SetDescription<TBuilder>(this TBuilder builder, string description)
where TBuilder : IWeaponViewBuilder
{
return builder.SetDescription<TBuilder, WeaponView>(description);
}
public static TBuilder SetIcon<TBuilder>(this TBuilder builder, Uri icon)
where TBuilder : IWeaponViewBuilder
{
return builder.SetIcon<TBuilder, WeaponView>(icon);
}
public static TBuilder SetId<TBuilder>(this TBuilder builder, WeaponId id)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.Id = id);
}
public static TBuilder SetLevel<TBuilder>(this TBuilder builder, string level)
where TBuilder : IWeaponViewBuilder
{
return builder.SetLevel<TBuilder, WeaponView>(level);
}
public static TBuilder SetLevelNumber<TBuilder>(this TBuilder builder, uint levelNumber)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.LevelNumber = levelNumber);
}
public static TBuilder SetMainProperty<TBuilder>(this TBuilder builder, WeaponStat? mainStat)
where TBuilder : IWeaponViewBuilder
{
return builder.SetMainProperty(mainStat is not null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : NameValueDefaults.String);
}
public static TBuilder SetMainProperty<TBuilder>(this TBuilder builder, NameValue<string> mainProperty)
where TBuilder : IWeaponViewBuilder
{
return builder.SetMainProperty<TBuilder, WeaponView>(mainProperty);
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IWeaponViewBuilder
{
return builder.SetName<TBuilder, WeaponView>(name);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IWeaponViewBuilder
{
return builder.SetQuality<TBuilder, WeaponView>(quality);
}
public static TBuilder SetSubProperty<TBuilder>(this TBuilder builder, NameDescription subProperty)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.SubProperty = subProperty);
}
public static TBuilder SetWeaponType<TBuilder>(this TBuilder builder, WeaponType weaponType)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.WeaponType = weaponType);
}
}

View File

@@ -5,8 +5,17 @@ using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory; namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述工厂
/// </summary>
[HighQuality] [HighQuality]
internal interface ISummaryFactory 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); ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token);
} }

View File

@@ -5,17 +5,21 @@ using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Intrinsic.Format; using Snap.Hutao.Model.Intrinsic.Format;
using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Service.AvatarInfo.Factory.Builder; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
using Snap.Hutao.Web.Enka.Model; using Snap.Hutao.Web.Enka.Model;
using System.Runtime.InteropServices;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using PropertyAvatar = Snap.Hutao.ViewModel.AvatarProperty.AvatarView;
using PropertyReliquary = Snap.Hutao.ViewModel.AvatarProperty.ReliquaryView;
using PropertyWeapon = Snap.Hutao.ViewModel.AvatarProperty.WeaponView;
namespace Snap.Hutao.Service.AvatarInfo.Factory; namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 单个角色工厂
/// </summary>
[HighQuality] [HighQuality]
internal sealed class SummaryAvatarFactory internal sealed class SummaryAvatarFactory
{ {
@@ -23,11 +27,16 @@ internal sealed class SummaryAvatarFactory
private readonly DateTimeOffset showcaseRefreshTime; private readonly DateTimeOffset showcaseRefreshTime;
private readonly DateTimeOffset gameRecordRefreshTime; private readonly DateTimeOffset gameRecordRefreshTime;
private readonly DateTimeOffset calculatorRefreshTime; 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; this.avatarInfo = avatarInfo.Info;
showcaseRefreshTime = avatarInfo.ShowcaseRefreshTime; showcaseRefreshTime = avatarInfo.ShowcaseRefreshTime;
@@ -35,51 +44,84 @@ internal sealed class SummaryAvatarFactory
calculatorRefreshTime = avatarInfo.CalculatorRefreshTime; calculatorRefreshTime = avatarInfo.CalculatorRefreshTime;
} }
public static AvatarView Create(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo) /// <summary>
{ /// 创建角色
return new SummaryAvatarFactory(context, avatarInfo).Create(); /// </summary>
} /// <returns>角色</returns>
public PropertyAvatar Create()
public AvatarView Create()
{ {
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull()); ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = context.IdAvatarMap[avatarInfo.AvatarId]; MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
AvatarView propertyAvatar = new AvatarViewBuilder() PropertyAvatar propertyAvatar = new()
.SetId(avatar.Id) {
.SetName(avatar.Name) // metadata part
.SetQuality(avatar.Quality) Id = avatar.Id,
.SetNameCard(AvatarNameCardPicConverter.AvatarToUri(avatar)) Name = avatar.Name,
.SetElement(ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore)) Quality = avatar.Quality,
.SetConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList) NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
.SetSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents()) Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
.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)
.SetCostumeIconOrDefault(avatarInfo, avatar)
.View;
// 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; 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) private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
{ {
List<ReliquaryView> reliquaryList = []; List<PropertyReliquary> reliquaryList = [];
WeaponView? weapon = null; PropertyWeapon? weapon = null;
foreach (ref readonly Equip equip in CollectionsMarshal.AsSpan(equipments)) foreach (Equip equip in equipments)
{ {
switch (equip.Flat.ItemType) switch (equip.Flat.ItemType)
{ {
case ItemType.ITEM_RELIQUARY: case ItemType.ITEM_RELIQUARY:
reliquaryList.Add(SummaryReliquaryFactory.Create(context, avatarInfo, equip)); SummaryReliquaryFactory summaryReliquaryFactory = new(metadataContext, avatarInfo, equip);
reliquaryList.Add(summaryReliquaryFactory.CreateReliquary());
break; break;
case ItemType.ITEM_WEAPON: case ItemType.ITEM_WEAPON:
weapon = CreateWeapon(equip); weapon = CreateWeapon(equip);
@@ -90,9 +132,9 @@ internal sealed class SummaryAvatarFactory
return new(reliquaryList, weapon); return new(reliquaryList, weapon);
} }
private WeaponView CreateWeapon(Equip equip) 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. // AffixMap can be null when it's a white weapon.
ArgumentNullException.ThrowIfNull(equip.Weapon); ArgumentNullException.ThrowIfNull(equip.Weapon);
@@ -116,29 +158,34 @@ internal sealed class SummaryAvatarFactory
ArgumentNullException.ThrowIfNull(equip.Weapon); ArgumentNullException.ThrowIfNull(equip.Weapon);
return new WeaponViewBuilder() return new()
.SetName(weapon.Name) {
.SetIcon(EquipIconConverter.IconNameToUri(weapon.Icon)) // NameIconDescription
.SetDescription(weapon.Description) Name = weapon.Name,
.SetLevel($"Lv.{equip.Weapon.Level.Value}") Icon = EquipIconConverter.IconNameToUri(weapon.Icon),
.SetQuality(weapon.Quality) Description = weapon.Description,
.SetMainProperty(mainStat)
.SetId(weapon.Id) // EquipBase
.SetLevelNumber(equip.Weapon.Level) Level = $"Lv.{equip.Weapon.Level.Value}",
.SetSubProperty(subProperty) Quality = weapon.Quality,
.SetAffixLevelNumber(affixLevel + 1) MainProperty = mainStat is not null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : NameValueDefaults.String,
.SetAffixName(weapon.Affix?.Name)
.SetAffixDescription(weapon.Affix?.Descriptions.Single(a => a.Level == affixLevel).Description) // Weapon
.SetWeaponType(weapon.WeaponType) Id = weapon.Id,
.View; LevelNumber = equip.Weapon.Level,
SubProperty = subProperty,
AffixLevelNumber = affixLevel + 1,
AffixName = weapon.Affix?.Name ?? string.Empty,
AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == affixLevel).Description ?? string.Empty,
};
} }
private readonly struct ReliquaryAndWeapon private readonly struct ReliquaryAndWeapon
{ {
public readonly List<ReliquaryView> Reliquaries; public readonly List<PropertyReliquary> Reliquaries;
public readonly WeaponView? Weapon; public readonly PropertyWeapon? Weapon;
public ReliquaryAndWeapon(List<ReliquaryView> reliquaries, WeaponView? weapon) public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon? weapon)
{ {
Reliquaries = reliquaries; Reliquaries = reliquaries;
Weapon = weapon; Weapon = weapon;

View File

@@ -13,6 +13,11 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
[HighQuality] [HighQuality]
internal static class SummaryAvatarProperties internal static class SummaryAvatarProperties
{ {
/// <summary>
/// 创建角色属性
/// </summary>
/// <param name="fightPropMap">属性映射</param>
/// <returns>列表</returns>
public static List<AvatarProperty> Create(Dictionary<FightProperty, float>? fightPropMap) public static List<AvatarProperty> Create(Dictionary<FightProperty, float>? fightPropMap)
{ {
if (fightPropMap is null) if (fightPropMap is null)
@@ -20,28 +25,33 @@ internal static class SummaryAvatarProperties
return []; return [];
} }
List<AvatarProperty> properties = AvatarProperty hpProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_HP, fightPropMap);
[ AvatarProperty atkProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, fightPropMap);
ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_HP, fightPropMap), AvatarProperty defProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, fightPropMap);
ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, fightPropMap), AvatarProperty emProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_ELEMENT_MASTERY, fightPropMap);
ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, fightPropMap), AvatarProperty critRateProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL, fightPropMap);
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_ELEMENT_MASTERY, fightPropMap), AvatarProperty critDMGProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, fightPropMap);
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL, fightPropMap), AvatarProperty chargeEffProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, fightPropMap);
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, fightPropMap),
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, fightPropMap) List<AvatarProperty> properties = new(9) { hpProp, atkProp, defProp, emProp, critRateProp, critDMGProp, chargeEffProp };
];
// 元素伤害 // 元素伤害
if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty, out float value) && value > 0) if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty, out float value))
{
if (value > 0)
{ {
properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value)); properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value));
} }
}
// 物伤 可以和其他元素伤害并存,所以分别判断 // 物伤 可以和其他元素伤害并存,所以分别判断
if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue) && addValue > 0) if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue))
{
if (addValue > 0)
{ {
properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue)); properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue));
} }
}
return properties; return properties;
} }

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory; namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -21,18 +20,22 @@ internal sealed partial class SummaryFactory : ISummaryFactory
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token) public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token)
{ {
SummaryFactoryMetadataContext context = await metadataService SummaryMetadataContext metadataContext = new()
.GetContextAsync<SummaryFactoryMetadataContext>(token) {
.ConfigureAwait(false); 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 IOrderedEnumerable<AvatarView> avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId)) .Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId))
.Select(a => SummaryAvatarFactory.Create(context, a)) .Select(a => new SummaryAvatarFactory(metadataContext, a).Create())
.OrderByDescending(a => a.Quality) .OrderByDescending(a => a.LevelNumber)
.ThenByDescending(a => a.LevelNumber) .ThenBy(a => a.Name);
.ThenBy(a => a.Element)
.ThenBy(a => a.Weapon?.WeaponType)
.ThenByDescending(a => a.FetterLevel);
return new() return new()
{ {

View File

@@ -1,14 +1,85 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService; 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.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory; namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述帮助类
/// </summary>
[HighQuality] [HighQuality]
internal static class SummaryHelper 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>
/// <param name="appendId">属性Id</param>
/// <returns>最大属性Id</returns>
public static ReliquarySubAffixId GetAffixMaxId(in ReliquarySubAffixId appendId) public static ReliquarySubAffixId GetAffixMaxId(in ReliquarySubAffixId appendId)
{ {
// axxxxx -> a // axxxxx -> a
@@ -20,13 +91,18 @@ internal static class SummaryHelper
1 => 2, 1 => 2,
2 => 3, 2 => 3,
3 or 4 or 5 => 4, 3 or 4 or 5 => 4,
_ => throw HutaoException.Throw($"不支持的 ReliquarySubAffixId: {appendId}"), _ => throw Must.NeverHappen(),
}; };
// axxxxb -> axxxx -> axxxx0 -> axxxxm // axxxxb -> axxxx -> axxxx0 -> axxxxm
return ((appendId / 10) * 10) + max; return ((appendId / 10) * 10) + max;
} }
/// <summary>
/// 获取百分比属性副词条分数
/// </summary>
/// <param name="appendId">id</param>
/// <returns>分数</returns>
public static float GetPercentSubAffixScore(in ReliquarySubAffixId appendId) public static float GetPercentSubAffixScore(in ReliquarySubAffixId appendId)
{ {
// 圣遗物相同类型副词条强化档位一共为 4/3/2 档 // 圣遗物相同类型副词条强化档位一共为 4/3/2 档
@@ -53,7 +129,25 @@ internal static class SummaryHelper
(1, 0) => 100F, (1, 0) => 100F,
(1, 1) => 80F, (1, 1) => 80F,
_ => throw HutaoException.Throw($"Unexpected AppendId: {appendId} Delta: {delta}"), _ => 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,34 +4,51 @@
using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Reliquary; using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
namespace Snap.Hutao.Service.AvatarInfo.Factory; namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述元数据上下文
/// 包含了所有制造简述需要的元数据
/// </summary>
[HighQuality] [HighQuality]
internal sealed class SummaryFactoryMetadataContext : IMetadataContext, internal sealed class SummaryMetadataContext
IMetadataDictionaryIdAvatarSource,
IMetadataDictionaryIdWeaponSource,
IMetadataDictionaryIdReliquaryAffixWeightSource,
IMetadataDictionaryIdReliquaryMainPropertySource,
IMetadataDictionaryIdReliquarySubAffixSource,
IMetadataDictionaryIdReliquarySource,
IMetadataListReliquaryMainAffixLevelSource
{ {
/// <summary>
/// 角色映射
/// </summary>
public Dictionary<AvatarId, MetadataAvatar> IdAvatarMap { get; set; } = default!; public Dictionary<AvatarId, MetadataAvatar> IdAvatarMap { get; set; } = default!;
/// <summary>
/// 武器映射
/// </summary>
public Dictionary<WeaponId, MetadataWeapon> IdWeaponMap { get; set; } = default!; public Dictionary<WeaponId, MetadataWeapon> IdWeaponMap { get; set; } = default!;
/// <summary>
/// 权重映射
/// </summary>
public Dictionary<AvatarId, ReliquaryAffixWeight> IdReliquaryAffixWeightMap { get; set; } = default!; 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 Dictionary<ReliquarySubAffixId, ReliquarySubAffix> IdReliquarySubAffixMap { get; set; } = default!;
public List<ReliquaryMainAffixLevel> ReliquaryMainAffixLevels { get; set; } = default!; /// <summary>
/// 圣遗物等级
/// </summary>
public List<ReliquaryMainAffixLevel> ReliquaryLevels { get; set; } = default!;
public Dictionary<ReliquaryId, MetadataReliquary> IdReliquaryMap { get; set; } = default!; /// <summary>
/// 圣遗物
/// </summary>
public List<MetadataReliquary> Reliquaries { get; set; } = default!;
} }

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary; using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.AvatarInfo.Factory.Builder;
using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.ViewModel.AvatarProperty;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
@@ -14,63 +13,76 @@ using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo.Factory; namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 圣遗物工厂
/// </summary>
[HighQuality] [HighQuality]
internal sealed class SummaryReliquaryFactory internal sealed class SummaryReliquaryFactory
{ {
private readonly SummaryFactoryMetadataContext metadataContext; private readonly SummaryMetadataContext metadataContext;
private readonly ModelAvatarInfo avatarInfo; private readonly ModelAvatarInfo avatarInfo;
private readonly Web.Enka.Model.Equip equip; 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.metadataContext = metadataContext;
this.avatarInfo = avatarInfo; this.avatarInfo = avatarInfo;
this.equip = equip; this.equip = equip;
} }
public static ReliquaryView Create(SummaryFactoryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip) /// <summary>
/// 构造圣遗物
/// </summary>
/// <returns>圣遗物</returns>
public ReliquaryView CreateReliquary()
{ {
return new SummaryReliquaryFactory(metadataContext, avatarInfo, equip).Create(); MetadataReliquary reliquary = metadataContext.Reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
}
public ReliquaryView Create()
{
MetadataReliquary reliquary = metadataContext.IdReliquaryMap[equip.ItemId];
ArgumentNullException.ThrowIfNull(equip.Reliquary); ArgumentNullException.ThrowIfNull(equip.Reliquary);
List<ReliquarySubProperty> subProperties = equip.Reliquary.AppendPropIdList.EmptyIfNull().SelectList(CreateSubProperty); List<ReliquarySubProperty> subProperty = equip.Reliquary.AppendPropIdList.EmptyIfNull().SelectList(CreateSubProperty);
ReliquaryViewBuilder reliquaryViewBuilder = new ReliquaryViewBuilder()
.SetName(reliquary.Name)
.SetIcon(RelicIconConverter.IconNameToUri(reliquary.Icon))
.SetDescription(reliquary.Description)
.SetLevel($"+{equip.Reliquary.Level - 1U}")
.SetQuality(reliquary.RankLevel);
int affixCount = GetSecondaryAffixCount(reliquary, equip.Reliquary); int affixCount = GetSecondaryAffixCount(reliquary, equip.Reliquary);
if (subProperties.Count > 0) ReliquaryView result = new()
{ {
reliquaryViewBuilder // NameIconDescription
.SetPrimarySubProperties(subProperties.GetRange(..^affixCount)) Name = reliquary.Name,
.SetSecondarySubProperties(subProperties.GetRange(^affixCount..)) Icon = RelicIconConverter.IconNameToUri(reliquary.Icon),
.SetComposedSubProperties(CreateComposedSubProperties(equip.Reliquary.AppendPropIdList)); Description = reliquary.Description,
ReliquaryMainAffixLevel relicLevel = metadataContext.ReliquaryMainAffixLevels.Single(r => r.Level == equip.Reliquary.Level && r.Rank == reliquary.RankLevel); // EquipBase
FightProperty property = metadataContext.IdReliquaryMainPropertyMap[equip.Reliquary.MainPropId]; Level = $"+{equip.Reliquary.Level - 1U}",
Quality = reliquary.RankLevel,
};
reliquaryViewBuilder if (subProperty.Count > 0)
.SetMainProperty(FightPropertyFormat.ToNameValue(property, relicLevel.PropertyMap[property])) {
.SetScore(ScoreReliquary(property, reliquary, relicLevel, subProperties)); result.PrimarySubProperties = subProperty.GetRange(..^affixCount);
result.SecondarySubProperties = subProperty.GetRange(^affixCount..);
ArgumentNullException.ThrowIfNull(equip.Flat.ReliquarySubstats);
result.ComposedSubProperties = CreateComposedSubProperties(equip.Reliquary.AppendPropIdList);
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);
} }
return reliquaryViewBuilder.View; return result;
} }
private static int GetSecondaryAffixCount(MetadataReliquary metaReliquary, Web.Enka.Model.Reliquary enkaReliquary) private static int GetSecondaryAffixCount(MetadataReliquary reliquary, Web.Enka.Model.Reliquary enkaReliquary)
{ {
// 强化词条个数 // 强化词条个数
return (metaReliquary.RankLevel, enkaReliquary.Level.Value) switch return (reliquary.RankLevel, enkaReliquary.Level.Value) switch
{ {
(QualityType.QUALITY_ORANGE, > 20U) => 5, (QualityType.QUALITY_ORANGE, > 20U) => 5,
(QualityType.QUALITY_ORANGE, > 16U) => 4, (QualityType.QUALITY_ORANGE, > 16U) => 4,
@@ -111,8 +123,18 @@ internal sealed class SummaryReliquaryFactory
info.Value += subAffix.Value; info.Value += subAffix.Value;
} }
HutaoException.ThrowIf(infos.Count > 4, "无效的圣遗物数据"); if (infos.Count > 4)
return infos.SelectList(info => info.ToReliquaryComposedSubProperty()); {
ThrowHelper.InvalidOperation("无效的圣遗物数据");
}
List<ReliquaryComposedSubProperty> results = [];
foreach (ref readonly SummaryReliquarySubPropertyCompositionInfo info in CollectionsMarshal.AsSpan(infos))
{
results.Add(info.ToReliquaryComposedSubProperty());
}
return results;
} }
private float ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryMainAffixLevel relicLevel, List<ReliquarySubProperty> subProperties) private float ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryMainAffixLevel relicLevel, List<ReliquarySubProperty> subProperties)
@@ -124,7 +146,7 @@ internal sealed class SummaryReliquaryFactory
// 从喵插件抓取的圣遗物评分权重 // 从喵插件抓取的圣遗物评分权重
// 部分复杂的角色暂时使用了默认值 // 部分复杂的角色暂时使用了默认值
ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default); 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); ArgumentNullException.ThrowIfNull(maxRelicLevel);
float percent = relicLevel.PropertyMap[property] / maxRelicLevel.PropertyMap[property]; float percent = relicLevel.PropertyMap[property] / maxRelicLevel.PropertyMap[property];
@@ -148,8 +170,9 @@ internal sealed class SummaryReliquaryFactory
FightProperty property = affix.Type; FightProperty property = affix.Type;
return new(property, FightPropertyFormat.FormatValue(property, affix.Value), ScoreSubAffix(appendPropId)); return new(property, FightPropertyFormat.FormatValue(property, affix.Value), ScoreSubAffix(appendPropId));
}
float ScoreSubAffix(in ReliquarySubAffixId appendId) private float ScoreSubAffix(in ReliquarySubAffixId appendId)
{ {
ReliquarySubAffix affix = metadataContext.IdReliquarySubAffixMap[appendId]; ReliquarySubAffix affix = metadataContext.IdReliquarySubAffixMap[appendId];
@@ -172,7 +195,7 @@ internal sealed class SummaryReliquaryFactory
// 最大同属性百分比数值 // 最大同属性百分比数值
ReliquarySubAffix maxPercentAffix = metadataContext.IdReliquarySubAffixMap[maxPercentAffixId]; ReliquarySubAffix maxPercentAffix = metadataContext.IdReliquarySubAffixMap[maxPercentAffixId];
HutaoException.ThrowIfNot( Must.Argument(
maxPercentAffix.Type maxPercentAffix.Type
is FightProperty.FIGHT_PROP_HP_PERCENT is FightProperty.FIGHT_PROP_HP_PERCENT
or FightProperty.FIGHT_PROP_ATTACK_PERCENT or FightProperty.FIGHT_PROP_ATTACK_PERCENT
@@ -185,5 +208,4 @@ internal sealed class SummaryReliquaryFactory
return weight * SummaryHelper.GetPercentSubAffixScore(appendId); return weight * SummaryHelper.GetPercentSubAffixScore(appendId);
} }
}
} }

View File

@@ -1,18 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo; using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo; namespace Snap.Hutao.Service.AvatarInfo;
internal interface IAvatarInfoDbService : IAppDbService<EntityAvatarInfo> internal interface IAvatarInfoDbService
{ {
void RemoveAvatarInfoRangeByUid(string uid); void RemoveAvatarInfoRangeByUid(string uid);
List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid); List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid);
ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default); ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid);
ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default); ValueTask RemoveAvatarInfoRangeByUidAsync(string uid);
} }

View File

@@ -19,5 +19,5 @@ internal interface IAvatarInfoService
/// <param name="refreshOption">刷新选项</param> /// <param name="refreshOption">刷新选项</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>总览数据</returns> /// <returns>总览数据</returns>
ValueTask<ValueResult<RefreshResultKind, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default); ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default);
} }

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.AvatarInfo;
/// 刷新结果 /// 刷新结果
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal enum RefreshResultKind internal enum RefreshResult
{ {
/// <summary> /// <summary>
/// 正常 /// 正常

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Transformer;
internal sealed class CalculateAvatarDetailAvatarInfoTransformer : IAvatarInfoTransformer<AvatarDetail> internal sealed class CalculateAvatarDetailAvatarInfoTransformer : IAvatarInfoTransformer<AvatarDetail>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Transform(ref readonly Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source) public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source)
{ {
// update skills // update skills
avatarInfo.SkillLevelMap = source.SkillList.ToDictionary(s => (SkillId)s.Id, s => (SkillLevel)s.LevelCurrent); avatarInfo.SkillLevelMap = source.SkillList.ToDictionary(s => (SkillId)s.Id, s => (SkillLevel)s.LevelCurrent);

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Transformer;
internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTransformer<Character> internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTransformer<Character>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Transform(ref readonly Web.Enka.Model.AvatarInfo avatarInfo, Character source) public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, Character source)
{ {
// update fetter // update fetter
avatarInfo.FetterInfo ??= new(); avatarInfo.FetterInfo ??= new();

View File

@@ -15,5 +15,5 @@ internal interface IAvatarInfoTransformer<in TSource>
/// </summary> /// </summary>
/// <param name="avatarInfo">基底角色Id必定存在</param> /// <param name="avatarInfo">基底角色Id必定存在</param>
/// <param name="source">源</param> /// <param name="source">源</param>
void Transform(ref readonly Web.Enka.Model.AvatarInfo avatarInfo, TSource source); void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, TSource source);
} }

View File

@@ -26,11 +26,11 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly AppOptions appOptions; private readonly AppOptions appOptions;
private HashSet<string>? currentBackgroundPathSet; private HashSet<string> currentBackgroundPathSet;
public async ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default) public async ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous)
{ {
HashSet<string> backgroundSet = await SkipOrInitBackgroundAsync(token).ConfigureAwait(false); HashSet<string> backgroundSet = await SkipOrInitBackgroundAsync().ConfigureAwait(false);
if (backgroundSet.Count <= 0) if (backgroundSet.Count <= 0)
{ {
@@ -79,7 +79,7 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
} }
} }
private async ValueTask<HashSet<string>> SkipOrInitBackgroundAsync(CancellationToken token = default) private async ValueTask<HashSet<string>> SkipOrInitBackgroundAsync()
{ {
switch (appOptions.BackgroundImageType) switch (appOptions.BackgroundImageType)
{ {
@@ -90,7 +90,7 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
string backgroundFolder = runtimeOptions.GetDataFolderBackgroundFolder(); string backgroundFolder = runtimeOptions.GetDataFolderBackgroundFolder();
currentBackgroundPathSet = Directory currentBackgroundPathSet = Directory
.EnumerateFiles(backgroundFolder, "*", SearchOption.AllDirectories) .GetFiles(backgroundFolder, "*.*", SearchOption.AllDirectories)
.Where(path => AllowedFormats.Contains(Path.GetExtension(path))) .Where(path => AllowedFormats.Contains(Path.GetExtension(path)))
.ToHashSet(); .ToHashSet();
} }
@@ -100,13 +100,13 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
} }
case BackgroundImageType.HutaoBing: case BackgroundImageType.HutaoBing:
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetBingWallpaperAsync(token), token).ConfigureAwait(false); await SetCurrentBackgroundPathSetAsync(client => client.GetBingWallpaperAsync()).ConfigureAwait(false);
break; break;
case BackgroundImageType.HutaoDaily: case BackgroundImageType.HutaoDaily:
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetTodayWallpaperAsync(token), token).ConfigureAwait(false); await SetCurrentBackgroundPathSetAsync(client => client.GetTodayWallpaperAsync()).ConfigureAwait(false);
break; break;
case BackgroundImageType.HutaoOfficialLauncher: case BackgroundImageType.HutaoOfficialLauncher:
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetLauncherWallpaperAsync(token), token).ConfigureAwait(false); await SetCurrentBackgroundPathSetAsync(client => client.GetLauncherWallpaperAsync()).ConfigureAwait(false);
break; break;
default: default:
currentBackgroundPathSet = []; currentBackgroundPathSet = [];
@@ -116,10 +116,10 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
currentBackgroundPathSet ??= []; currentBackgroundPathSet ??= [];
return currentBackgroundPathSet; return currentBackgroundPathSet;
async Task SetCurrentBackgroundPathSetAsync(Func<HutaoWallpaperClient, CancellationToken, ValueTask<Response<Wallpaper>>> responseFactory, CancellationToken token = default) async Task SetCurrentBackgroundPathSetAsync(Func<HutaoWallpaperClient, ValueTask<Response<Wallpaper>>> responseFactory)
{ {
HutaoWallpaperClient wallpaperClient = serviceProvider.GetRequiredService<HutaoWallpaperClient>(); HutaoWallpaperClient wallpaperClient = serviceProvider.GetRequiredService<HutaoWallpaperClient>();
Response<Wallpaper> response = await responseFactory(wallpaperClient, token).ConfigureAwait(false); Response<Wallpaper> response = await responseFactory(wallpaperClient).ConfigureAwait(false);
if (response is { Data: Wallpaper wallpaper }) if (response is { Data: Wallpaper wallpaper })
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();

View File

@@ -5,5 +5,5 @@ namespace Snap.Hutao.Service.BackgroundImage;
internal interface IBackgroundImageService internal interface IBackgroundImageService
{ {
ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default); ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous);
} }

View File

@@ -2,8 +2,9 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Cultivation; namespace Snap.Hutao.Service.Cultivation;
@@ -14,90 +15,182 @@ internal sealed partial class CultivationDbService : ICultivationDbService
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId) public List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId)
{ {
return this.List<InventoryItem>(i => i.ProjectId == projectId); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<InventoryItem> result = appDbContext.InventoryItems.AsNoTracking().Where(a => a.ProjectId == projectId);
return [.. result];
}
} }
public ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default) public async ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId)
{ {
return this.ListAsync<InventoryItem>(i => i.ProjectId == projectId, token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.InventoryItems
.AsNoTracking()
.Where(a => a.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
}
} }
public ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default) public async ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId)
{ {
return this.ListAsync<CultivateEntry>(e => e.ProjectId == projectId, token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
}
} }
public ValueTask<List<CultivateEntry>> GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(Guid projectId, CancellationToken token = default) public async ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId)
{ {
return this.ListAsync<CultivateEntry, CultivateEntry>(query => query.Where(e => e.ProjectId == projectId).Include(e => e.LevelInformation), token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.Include(e => e.LevelInformation)
.ToListAsync()
.ConfigureAwait(false);
}
} }
public ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId, CancellationToken token = default) public async ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId)
{ {
return this.ListAsync<CultivateItem, CultivateItem>(query => query.Where(i => i.EntryId == entryId).OrderBy(i => i.ItemId), token); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateItems
.Where(i => i.EntryId == entryId)
.OrderBy(i => i.ItemId)
.ToListAsync()
.ConfigureAwait(false);
}
} }
public async ValueTask RemoveCultivateEntryByIdAsync(Guid entryId, CancellationToken token = default) public async ValueTask RemoveCultivateEntryByIdAsync(Guid entryId)
{ {
await this.DeleteByInnerIdAsync<CultivateEntry>(entryId, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateEntries
.ExecuteDeleteWhereAsync(i => i.InnerId == entryId)
.ConfigureAwait(false);
}
} }
public void UpdateCultivateItem(CultivateItem item) public void UpdateCultivateItem(CultivateItem item)
{ {
this.Update(item); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.CultivateItems.UpdateAndSave(item);
}
} }
public async ValueTask UpdateCultivateItemAsync(CultivateItem item, CancellationToken token = default) public async ValueTask UpdateCultivateItemAsync(CultivateItem item)
{ {
await this.UpdateAsync(item, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateItems.UpdateAndSaveAsync(item).ConfigureAwait(false);
}
} }
public async ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId, CancellationToken token = default) public async ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId)
{ {
return await this.SingleOrDefaultAsync<CultivateEntry>(e => e.ProjectId == projectId && e.Id == itemId, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.SingleOrDefaultAsync(e => e.ProjectId == projectId && e.Id == itemId)
.ConfigureAwait(false);
}
} }
public async ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default) public async ValueTask AddCultivateEntryAsync(CultivateEntry entry)
{ {
await this.AddAsync(entry, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateEntries.AddAndSaveAsync(entry).ConfigureAwait(false);
}
} }
public async ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId, CancellationToken token = default) public async ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId)
{ {
await this.DeleteAsync<CultivateItem>(i => i.EntryId == entryId, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateItems
.ExecuteDeleteWhereAsync(i => i.EntryId == entryId)
.ConfigureAwait(false);
}
} }
public async ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default) public async ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd)
{ {
await this.AddRangeAsync(toAdd, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false);
}
} }
public async ValueTask AddCultivateProjectAsync(CultivateProject project, CancellationToken token = default) public async ValueTask AddCultivateProjectAsync(CultivateProject project)
{ {
await this.AddAsync(project, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateProjects.AddAndSaveAsync(project).ConfigureAwait(false);
}
} }
public async ValueTask RemoveCultivateProjectByIdAsync(Guid projectId, CancellationToken token = default) public async ValueTask RemoveCultivateProjectByIdAsync(Guid projectId)
{ {
await this.DeleteByInnerIdAsync<CultivateProject>(projectId, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateProjects
.ExecuteDeleteWhereAsync(p => p.InnerId == projectId)
.ConfigureAwait(false);
}
} }
public ObservableCollection<CultivateProject> GetCultivateProjectCollection() public ObservableCollection<CultivateProject> GetCultivateProjectCollection()
{ {
return this.ObservableCollection<CultivateProject>(); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.CultivateProjects.AsNoTracking().ToObservableCollection();
}
} }
public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId, CancellationToken token = default) public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId)
{ {
await this.DeleteAsync<CultivateEntryLevelInformation>(l => l.EntryId == entryId, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.LevelInformations.ExecuteDeleteWhereAsync(l => l.EntryId == entryId).ConfigureAwait(false);
}
} }
public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation, CancellationToken token = default) public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation)
{ {
await this.AddAsync(levelInformation, token).ConfigureAwait(false); using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.LevelInformations.AddAndSaveAsync(levelInformation).ConfigureAwait(false);
}
} }
} }

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