mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
40 Commits
1.9.9
...
feat/HttpC
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8703c3a598 | ||
|
|
80d6d5eb2b | ||
|
|
f682bb57e8 | ||
|
|
486c6eb308 | ||
|
|
0629f7c4c9 | ||
|
|
b49288a98f | ||
|
|
ee99d0b665 | ||
|
|
72b62aa9c6 | ||
|
|
6b031e1866 | ||
|
|
59c03c7f3b | ||
|
|
c03a96b44f | ||
|
|
d5a97903d3 | ||
|
|
7f998dc87f | ||
|
|
2367c4759d | ||
|
|
043e3f07d8 | ||
|
|
cd075c4dab | ||
|
|
65252f1f69 | ||
|
|
f5dd5f4c1d | ||
|
|
27ce55f3f7 | ||
|
|
311941bb89 | ||
|
|
07e2489cab | ||
|
|
699ac60aaf | ||
|
|
dc7bc7e35d | ||
|
|
bf67fcf3a2 | ||
|
|
5093246571 | ||
|
|
94fe192581 | ||
|
|
c679032387 | ||
|
|
d119b056c7 | ||
|
|
ab95ce8ce8 | ||
|
|
0ede5b158f | ||
|
|
6c9a98c2c9 | ||
|
|
a725fc0e9e | ||
|
|
5251dd9343 | ||
|
|
0b71053bf9 | ||
|
|
a91499171d | ||
|
|
e041def070 | ||
|
|
2e81ddf3f4 | ||
|
|
4d988e5500 | ||
|
|
36aff679c0 | ||
|
|
d490393108 |
BIN
res/Banner3-large-cn.psd
Normal file
BIN
res/Banner3-large-cn.psd
Normal file
Binary file not shown.
BIN
res/Banner3-large.psd
Normal file
BIN
res/Banner3-large.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/abyss.psd
Normal file
BIN
res/Store/chs/abyss.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/achievement.psd
Normal file
BIN
res/Store/chs/achievement.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/character-data.psd
Normal file
BIN
res/Store/chs/character-data.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/lancher.psd
Normal file
BIN
res/Store/chs/lancher.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/realtime-notes.psd
Normal file
BIN
res/Store/chs/realtime-notes.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/wish.psd
Normal file
BIN
res/Store/chs/wish.psd
Normal file
Binary file not shown.
BIN
res/Store/en/abyss.psd
Normal file
BIN
res/Store/en/abyss.psd
Normal file
Binary file not shown.
BIN
res/Store/en/achievement.psd
Normal file
BIN
res/Store/en/achievement.psd
Normal file
Binary file not shown.
BIN
res/Store/en/character-data.psd
Normal file
BIN
res/Store/en/character-data.psd
Normal file
Binary file not shown.
BIN
res/Store/en/lancher.psd
Normal file
BIN
res/Store/en/lancher.psd
Normal file
Binary file not shown.
BIN
res/Store/en/realtime-notes.psd
Normal file
BIN
res/Store/en/realtime-notes.psd
Normal file
Binary file not shown.
BIN
res/Store/en/wish.psd
Normal file
BIN
res/Store/en/wish.psd
Normal file
Binary file not shown.
@@ -110,7 +110,6 @@ dotnet_diagnostic.SA1642.severity = none
|
||||
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
# SA1208: System using directives should be placed before other using directives
|
||||
dotnet_diagnostic.SA1208.severity = none
|
||||
@@ -321,7 +320,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
|
||||
csharp_style_prefer_primary_constructors = false:none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
@@ -11,6 +12,7 @@ public sealed class DependencyInjectionTest
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.AddLogging(builder => builder.AddConsole())
|
||||
.BuildServiceProvider();
|
||||
|
||||
[TestMethod]
|
||||
@@ -41,6 +43,13 @@ public sealed class DependencyInjectionTest
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoggerWithInterfaceTypeCanBeResolved()
|
||||
{
|
||||
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
|
||||
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -10,8 +10,6 @@ using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
|
||||
{
|
||||
public TButton Button { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBaseBuilderExtension
|
||||
{
|
||||
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Content = content);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Command = command);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBuilderExtension
|
||||
{
|
||||
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
|
||||
{
|
||||
return builder.SetContent<ButtonBuilder, Button>(content);
|
||||
}
|
||||
|
||||
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
|
||||
{
|
||||
return builder.SetCommand<ButtonBuilder, Button>(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal interface IButtonBaseBuilder<TButton> : IBuilder
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
TButton Button { get; }
|
||||
}
|
||||
@@ -35,7 +35,7 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
|
||||
try
|
||||
{
|
||||
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), HutaoExceptionKind.ImageCacheInvalidUri, SH.ControlImageCachedImageInvalidResourceUri);
|
||||
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
|
||||
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
|
||||
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder;
|
||||
namespace Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
internal static class BuilderExtension
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal interface IBuilder;
|
||||
@@ -5,6 +5,9 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.ViewModel.Guide;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.IO;
|
||||
@@ -36,6 +39,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ImageCache> logger;
|
||||
|
||||
@@ -169,38 +173,49 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
while (retryCount < 3)
|
||||
{
|
||||
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
|
||||
.Create()
|
||||
.SetRequestUri(uri)
|
||||
|
||||
if (message.IsSuccessStatusCode)
|
||||
// These headers are only available for our own api
|
||||
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
|
||||
.Get();
|
||||
|
||||
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
|
||||
{
|
||||
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return;
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,6 @@ namespace Snap.Hutao.Core.Database;
|
||||
[HighQuality]
|
||||
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)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -27,27 +20,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -55,27 +34,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -83,27 +48,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -111,18 +62,11 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -136,11 +80,11 @@ internal static class DbSetExtension
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,13 @@ internal static class DependencyInjection
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
|
||||
.AddLogging(builder =>
|
||||
{
|
||||
builder
|
||||
.SetMinimumLevel(LogLevel.Trace)
|
||||
.AddDebug()
|
||||
.AddConsoleWindow();
|
||||
})
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
|
||||
@@ -18,13 +18,22 @@ internal static class ServiceProviderExtension
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
|
||||
public static bool IsDisposed(this IServiceProvider? serviceProvider)
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serviceProvider is ServiceProvider serviceProviderImpl)
|
||||
{
|
||||
return GetPrivateDisposed(serviceProviderImpl);
|
||||
}
|
||||
|
||||
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
|
||||
}
|
||||
|
||||
// private bool _disposed;
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
|
||||
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
|
||||
}
|
||||
@@ -5,50 +5,61 @@ namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal sealed class HutaoException : Exception
|
||||
{
|
||||
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
|
||||
: this(message, innerException)
|
||||
{
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
private HutaoException(string message, Exception? innerException)
|
||||
public HutaoException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public HutaoExceptionKind Kind { get; private set; }
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException Throw(HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
public static HutaoException Throw(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
|
||||
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
public static void ThrowIf(bool condition, string message, Exception? innerException = default)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ThrowIfNot(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
public static void ThrowIfNot(bool condition, string message, Exception? innerException = default)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
}
|
||||
|
||||
public static HutaoException ServiceTypeCastFailed<TFrom, TTo>(string name, Exception? innerException = default)
|
||||
{
|
||||
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||
throw new HutaoException(HutaoExceptionKind.ServiceTypeCastFailed, message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default)
|
||||
{
|
||||
string message = SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id);
|
||||
throw new HutaoException(HutaoExceptionKind.GachaStatisticsInvalidItemId, message, innerException);
|
||||
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)
|
||||
{
|
||||
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||
throw new InvalidCastException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
|
||||
{
|
||||
throw new NotSupportedException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
|
||||
{
|
||||
return new OperationCanceledException(message, innerException);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@ internal enum HutaoExceptionKind
|
||||
None,
|
||||
|
||||
// Foundation
|
||||
ServiceTypeCastFailed,
|
||||
ImageCacheInvalidUri,
|
||||
DatabaseCorrupted,
|
||||
UserdataCorrupted,
|
||||
|
||||
// IO
|
||||
FileSystemCreateFileInsufficientPermissions,
|
||||
@@ -18,4 +19,5 @@ internal enum HutaoExceptionKind
|
||||
// Service
|
||||
GachaStatisticsInvalidItemId,
|
||||
GameFpsUnlockingFailed,
|
||||
GameConfigInvalidChannelOptions,
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -77,34 +78,36 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
Memory<byte> buffer = new byte[bufferSize];
|
||||
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent())
|
||||
{
|
||||
int totalBytesRead = 0;
|
||||
int bytesReadAfterPreviousReport = 0;
|
||||
do
|
||||
Memory<byte> buffer = memoryOwner.Memory;
|
||||
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||
if (bytesRead <= 0)
|
||||
int totalBytesRead = 0;
|
||||
int bytesReadAfterPreviousReport = 0;
|
||||
do
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
break;
|
||||
}
|
||||
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||
if (bytesRead <= 0)
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
|
||||
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
bytesReadAfterPreviousReport += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
totalBytesRead += bytesRead;
|
||||
bytesReadAfterPreviousReport += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
|
||||
{
|
||||
progress.Report(new(bytesReadAfterPreviousReport));
|
||||
bytesReadAfterPreviousReport = 0;
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
@@ -51,26 +52,30 @@ internal class StreamCopyWorker<TStatus>
|
||||
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
Memory<byte> buffer = new byte[bufferSize];
|
||||
|
||||
do
|
||||
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize))
|
||||
{
|
||||
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
break;
|
||||
}
|
||||
Memory<byte> buffer = memoryOwner.Memory;
|
||||
|
||||
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
|
||||
do
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
|
||||
if (bytesRead is 0)
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
break;
|
||||
}
|
||||
|
||||
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
|
||||
{
|
||||
progress.Report(statusFactory(totalBytesRead));
|
||||
stopwatch = ValueStopwatch.StartNew();
|
||||
}
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ internal readonly struct TempFile : IDisposable
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
HutaoException.Throw(HutaoExceptionKind.FileSystemCreateFileInsufficientPermissions, SH.CoreIOTempFileCreateFail, ex);
|
||||
HutaoException.Throw(SH.CoreIOTempFileCreateFail, ex);
|
||||
}
|
||||
|
||||
if (delete)
|
||||
|
||||
@@ -116,15 +116,15 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
// If it's the first time launch, we show the guide window anyway.
|
||||
// Otherwise, we check if there's any unfulfilled resource category present.
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
|
||||
{
|
||||
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
|
||||
{
|
||||
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin);
|
||||
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
|
||||
}
|
||||
}
|
||||
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) < GuideState.Completed)
|
||||
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
serviceProvider.GetRequiredService<GuideWindow>();
|
||||
|
||||
@@ -49,7 +49,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
|
||||
{
|
||||
byte[] content = new byte[header->ContentLength];
|
||||
serverStream.ReadAtLeast(content, header->ContentLength, false);
|
||||
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, HutaoExceptionKind.PrivateNamedPipeContentHashIncorrect, "PipePacket Content Hash incorrect");
|
||||
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ internal static class LoggerFactoryExtension
|
||||
{
|
||||
builder.Services.AddSingleton<ConsoleWindowLifeTime>();
|
||||
|
||||
builder.AddSimpleConsole();
|
||||
builder.AddSimpleConsole(options =>
|
||||
{
|
||||
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff ";
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,15 @@ internal static class RuntimeOptionsExtension
|
||||
|
||||
public static string GetDataFolderServerCacheFolder(this RuntimeOptions options)
|
||||
{
|
||||
return Path.Combine(options.DataFolder, "ServerCache");
|
||||
string directory = Path.Combine(options.DataFolder, "ServerCache");
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
public static string GetDataFolderBackgroundFolder(this RuntimeOptions options)
|
||||
{
|
||||
return Path.Combine(options.DataFolder, "Background");
|
||||
string directory = Path.Combine(options.DataFolder, "Background");
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ internal static class SettingKeys
|
||||
#region Application
|
||||
public const string LaunchTimes = "LaunchTimes";
|
||||
public const string DataFolderPath = "DataFolderPath";
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
public const string Major1Minor10Revision0GuideState = "Major1Minor10Revision0GuideState1";
|
||||
public const string StaticResourceImageQuality = "StaticResourceImageQuality";
|
||||
public const string StaticResourceImageArchive = "StaticResourceImageArchive";
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
|
||||
#endregion
|
||||
@@ -60,6 +62,10 @@ internal static class SettingKeys
|
||||
#endregion
|
||||
|
||||
#region Obsolete
|
||||
|
||||
[Obsolete("重置新手引导状态")]
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
|
||||
[Obsolete("重置调试控制台开关")]
|
||||
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
|
||||
#endregion
|
||||
|
||||
@@ -118,14 +118,6 @@ internal static partial class EnumerableExtension
|
||||
collection.RemoveAt(collection.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到新类型的列表
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">原始类型</typeparam>
|
||||
/// <typeparam name="TResult">新类型</typeparam>
|
||||
/// <param name="list">列表</param>
|
||||
/// <param name="selector">选择器</param>
|
||||
/// <returns>新类型的列表</returns>
|
||||
[Pure]
|
||||
public static List<TResult> SelectList<TSource, TResult>(this List<TSource> list, Func<TSource, TResult> selector)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
xmlns:shvg="using:Snap.Hutao.View.Guide"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
<Grid x:Name="RootGrid" Background="{ThemeResource SolidBackgroundFillColorBaseBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
|
||||
@@ -14,10 +14,10 @@ namespace Snap.Hutao;
|
||||
internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
|
||||
{
|
||||
private const int MinWidth = 1000;
|
||||
private const int MinHeight = 600;
|
||||
private const int MinHeight = 650;
|
||||
|
||||
private const int MaxWidth = 1200;
|
||||
private const int MaxHeight = 750;
|
||||
private const int MaxHeight = 800;
|
||||
|
||||
private readonly WindowOptions windowOptions;
|
||||
|
||||
|
||||
@@ -27,4 +27,27 @@ internal sealed partial class IdentifyMonitorWindow : Window
|
||||
}
|
||||
|
||||
public string Monitor { get; private set; }
|
||||
|
||||
public static async ValueTask IdentifyAllMonitorsAsync(int secondsDelay)
|
||||
{
|
||||
List<IdentifyMonitorWindow> windows = [];
|
||||
|
||||
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
|
||||
for (int i = 0; i < displayAreas.Count; i++)
|
||||
{
|
||||
windows.Add(new IdentifyMonitorWindow(displayAreas[i], i + 1));
|
||||
}
|
||||
|
||||
foreach (IdentifyMonitorWindow window in windows)
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
|
||||
await Delay.FromSeconds(secondsDelay).ConfigureAwait(true);
|
||||
|
||||
foreach (IdentifyMonitorWindow window in windows)
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Abstraction;
|
||||
|
||||
internal interface IAppDbEntity
|
||||
{
|
||||
Guid InnerId { get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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; }
|
||||
}
|
||||
@@ -16,8 +16,8 @@ namespace Snap.Hutao.Model.Entity;
|
||||
[HighQuality]
|
||||
[Table("achievements")]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal sealed class Achievement
|
||||
: IEquatable<Achievement>,
|
||||
internal sealed class Achievement : IAppDbEntityHasArchive,
|
||||
IEquatable<Achievement>,
|
||||
IDbMappingForeignKeyFrom<Achievement, AchievementId>,
|
||||
IDbMappingForeignKeyFrom<Achievement, UIAFItem>
|
||||
{
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>Open UIAF Json File</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>Multiple identical achievement IDs found in a single achievement archive</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>Buka berkas UIAF Json</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>Terdapat beberapa ID pencapaian yang identik dalam satu arsip pencapaian</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>UIAF Json ファイルを開く</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>複数の同一アチーブメント Idがアーカイブに混在しています</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>打开 UIAF Json 文件</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>한 업적 아카이브에서 Id 동일한 업적 발견됨</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>Abrir arquivo Json UIAF</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>Várias IDs de conquistas idênticas encontradas em um único arquivo de conquistas</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>打开 UIAF Json 文件</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>单个成就存档内发现多个相同的成就 Id</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
@@ -1334,6 +1334,12 @@
|
||||
<data name="ViewDialogLaunchGameAccountTitle" xml:space="preserve">
|
||||
<value>为账号命名</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGameConfigurationFixDialogHint" xml:space="preserve">
|
||||
<value>请选择当前游戏路径对应的游戏服务器</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGameConfigurationFixDialogTitle" xml:space="preserve">
|
||||
<value>正在修复配置文件</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGamePackageConvertHint" xml:space="preserve">
|
||||
<value>转换可能需要花费一段时间,请勿关闭胡桃</value>
|
||||
</data>
|
||||
@@ -1376,6 +1382,9 @@
|
||||
<data name="ViewGachaLogHeader" xml:space="preserve">
|
||||
<value>祈愿记录</value>
|
||||
</data>
|
||||
<data name="ViewGuideStaticResourceDownloadSize" xml:space="preserve">
|
||||
<value>预计下载大小:{0}</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepAgreementIHaveReadText" xml:space="preserve">
|
||||
<value>我已阅读并同意</value>
|
||||
</data>
|
||||
@@ -1391,6 +1400,12 @@
|
||||
<data name="ViewGuideStepAgreementTermOfService" xml:space="preserve">
|
||||
<value>用户使用协议与法律声明</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepCommonSetting" xml:space="preserve">
|
||||
<value>基础设置</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepCommonSettingHint" xml:space="preserve">
|
||||
<value>稍后可以在设置中修改</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepDocument" xml:space="preserve">
|
||||
<value>文档</value>
|
||||
</data>
|
||||
@@ -1401,7 +1416,7 @@
|
||||
<value>安装完成后重启胡桃以查看是否正常生效</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepEnvironmentFontDescription1" xml:space="preserve">
|
||||
<value>如果上方的图标中存在乱码,请前往</value>
|
||||
<value>如果上方的图标中存在乱码或方块字,请前往</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepEnvironmentFontDescription2" xml:space="preserve">
|
||||
<value>下载并自行安装图标字体</value>
|
||||
@@ -1418,6 +1433,24 @@
|
||||
<data name="ViewGuideStepStaticResource" xml:space="preserve">
|
||||
<value>资源</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSetting" xml:space="preserve">
|
||||
<value>图像资源设置</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingHint" xml:space="preserve">
|
||||
<value>* 除非你卸载并重新安装胡桃,否则你将无法更改这些设置</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingMinimumHeader" xml:space="preserve">
|
||||
<value>图片资源包体</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingMinimumOff" xml:space="preserve">
|
||||
<value>完整包体</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingMinimumOn" xml:space="preserve">
|
||||
<value>精简包体</value>
|
||||
</data>
|
||||
<data name="ViewGuideStepStaticResourceSettingQualityHeader" xml:space="preserve">
|
||||
<value>图片资源质量</value>
|
||||
</data>
|
||||
<data name="ViewHutaoDatabaseHeader" xml:space="preserve">
|
||||
<value>深渊统计</value>
|
||||
</data>
|
||||
@@ -1607,6 +1640,12 @@
|
||||
<data name="ViewModelGuideActionStaticResourceBegin" xml:space="preserve">
|
||||
<value>下载资源文件中,请稍候</value>
|
||||
</data>
|
||||
<data name="ViewModelGuideStaticResourceQualityHigh" xml:space="preserve">
|
||||
<value>高质量</value>
|
||||
</data>
|
||||
<data name="ViewModelGuideStaticResourceQualityRaw" xml:space="preserve">
|
||||
<value>原图</value>
|
||||
</data>
|
||||
<data name="ViewModelHutaoPassportEmailNotValidHint" xml:space="preserve">
|
||||
<value>请输入正确的邮箱</value>
|
||||
</data>
|
||||
@@ -1625,6 +1664,12 @@
|
||||
<data name="ViewModelLaunchGameEnsureGameResourceFail" xml:space="preserve">
|
||||
<value>切换服务器失败</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameFixConfigurationFileButtonText" xml:space="preserve">
|
||||
<value>修复配置文件</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameFixConfigurationFileSuccess" xml:space="preserve">
|
||||
<value>修复完成</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameIdentifyMonitorsAction" xml:space="preserve">
|
||||
<value>识别显示器</value>
|
||||
</data>
|
||||
@@ -1637,6 +1682,9 @@
|
||||
<data name="ViewModelLaunchGameSchemeNotSelected" xml:space="preserve">
|
||||
<value>尚未选择任何服务器</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameSetGamePathButtonText" xml:space="preserve">
|
||||
<value>设置游戏目录</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>切换账号失败</value>
|
||||
</data>
|
||||
@@ -2834,6 +2882,15 @@
|
||||
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
|
||||
<value>上传数据</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageDownloadContent" xml:space="preserve">
|
||||
<value>是否立即下载</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageDownloadFailedMessage" xml:space="preserve">
|
||||
<value>下载更新失败</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageDownloadTitle" xml:space="preserve">
|
||||
<value>胡桃 {0} 版本已发布</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
|
||||
<value>是否立即安装?</value>
|
||||
</data>
|
||||
@@ -2843,6 +2900,9 @@
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewTitleUpdatePackageInstallingContent" xml:space="preserve">
|
||||
<value>正在安装更新</value>
|
||||
</data>
|
||||
<data name="ViewToolHeader" xml:space="preserve">
|
||||
<value>工具</value>
|
||||
</data>
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>Открыть UIAF Json файл</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>В одном архиве достижений обнаружено несколько одинаковых идентификаторов достижений</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -60,45 +60,45 @@
|
||||
: 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: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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<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:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -653,7 +653,7 @@
|
||||
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
|
||||
<value>打開 UIAF Json 文件</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
|
||||
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
|
||||
<value>單個成就存檔內發現多個相同的成就 Id</value>
|
||||
</data>
|
||||
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Entity.Abstraction;
|
||||
|
||||
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.Execute(dbset => dbset.ExecuteDeleteWhere(e => e.InnerId == entity.InnerId));
|
||||
}
|
||||
|
||||
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class, IAppDbEntity
|
||||
{
|
||||
return service.ExecuteAsync((dbset, token) => dbset.ExecuteDeleteWhereAsync(e => e.InnerId == entity.InnerId, token), token);
|
||||
}
|
||||
|
||||
public static List<TEntity> ListByArchiveId<TEntity>(this IAppDbService<TEntity> service, Guid archiveId)
|
||||
where TEntity : class, IAppDbEntityHasArchive
|
||||
{
|
||||
return service.Query(query => query.Where(e => e.ArchiveId == archiveId).ToList());
|
||||
}
|
||||
|
||||
public static ValueTask<List<TEntity>> ListByArchiveIdAsync<TEntity>(this IAppDbService<TEntity> service, Guid archiveId, CancellationToken token = default)
|
||||
where TEntity : class, IAppDbEntityHasArchive
|
||||
{
|
||||
return service.QueryAsync((query, token) => query.Where(e => e.ArchiveId == archiveId).ToListAsync(token), token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
|
||||
internal static class AppDbServiceCollectionExtension
|
||||
{
|
||||
public static List<TEntity> List<TEntity>(this IAppDbService<TEntity> service)
|
||||
where TEntity : class
|
||||
{
|
||||
return service.Query(query => query.ToList());
|
||||
}
|
||||
|
||||
public static List<TEntity> List<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
|
||||
where TEntity : class
|
||||
{
|
||||
return service.Query(query => query.Where(predicate).ToList());
|
||||
}
|
||||
|
||||
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
return service.QueryAsync((query, token) => query.ToListAsync(token), token);
|
||||
}
|
||||
|
||||
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
return service.QueryAsync((query, token) => query.Where(predicate).ToListAsync(token), token);
|
||||
}
|
||||
|
||||
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service)
|
||||
where TEntity : class
|
||||
{
|
||||
return service.Query(query => query.ToObservableCollection());
|
||||
}
|
||||
|
||||
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
|
||||
where TEntity : class
|
||||
{
|
||||
return service.Query(query => query.Where(predicate).ToObservableCollection());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// 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 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 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);
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,10 @@ namespace Snap.Hutao.Service.Abstraction;
|
||||
/// 数据库存储选项的设置
|
||||
/// </summary>
|
||||
[ConstructorGenerated]
|
||||
internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbStoreOptions>
|
||||
internal abstract partial class DbStoreOptions : ObservableObject
|
||||
{
|
||||
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 = "")
|
||||
{
|
||||
return GetOption(ref storage, key, () => defaultValue);
|
||||
@@ -49,13 +39,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
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)
|
||||
{
|
||||
return GetOption(ref storage, key, () => defaultValue);
|
||||
@@ -78,13 +61,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
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)
|
||||
{
|
||||
return GetOption(ref storage, key, () => defaultValue);
|
||||
@@ -107,15 +83,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
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]
|
||||
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, [DisallowNull] T defaultValue)
|
||||
{
|
||||
@@ -160,13 +127,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
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)
|
||||
{
|
||||
if (!SetProperty(ref storage, value, propertyName))
|
||||
@@ -182,14 +142,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
bool set = SetProperty(ref storage, value, propertyName);
|
||||
@@ -208,13 +160,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
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)
|
||||
{
|
||||
if (!SetProperty(ref storage, value, propertyName))
|
||||
@@ -230,15 +175,6 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!SetProperty(ref storage, value, propertyName))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
|
||||
internal interface IAppInfrastructureService
|
||||
{
|
||||
IServiceProvider ServiceProvider { get; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
|
||||
internal interface IAppService;
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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>();
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Collection;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
@@ -21,197 +23,155 @@ internal sealed partial class AchievementDbBulkOperation
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
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)
|
||||
{
|
||||
logger.LogInformation("Perform {Method} Operation for archive: {Id}, Aggressive: {Aggressive}", nameof(Merge), archiveId, aggressive);
|
||||
logger.LogInformation("Perform merge operation for [Archive: {Id}], [Aggressive: {Aggressive}]", archiveId, aggressive);
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
AppDbContext appDbContext = scope.GetAppDbContext();
|
||||
|
||||
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
|
||||
.AsNoTracking()
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.OrderBy(a => a.Id);
|
||||
|
||||
int add = 0;
|
||||
int update = 0;
|
||||
(int add, int update) = (0, 0);
|
||||
|
||||
using (IEnumerator<EntityAchievement> entityEnumerator = oldData.GetEnumerator())
|
||||
using (TwoEnumerbleEnumerator<EntityAchievement, UIAFItem> enumerator = new(oldData, items))
|
||||
{
|
||||
using (IEnumerator<UIAFItem> uiafEnumerator = items.GetEnumerator())
|
||||
(bool moveEntity, bool moveUIAF) = (true, true);
|
||||
|
||||
while (true)
|
||||
{
|
||||
bool moveEntity = true;
|
||||
bool moveUIAF = true;
|
||||
|
||||
while (true)
|
||||
if (!enumerator.MoveNext(ref moveEntity, ref moveUIAF))
|
||||
{
|
||||
bool moveEntityResult = moveEntity && entityEnumerator.MoveNext();
|
||||
bool moveUIAFResult = moveUIAF && uiafEnumerator.MoveNext();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!(moveEntityResult || moveUIAFResult))
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityAchievement? entity = entityEnumerator.Current;
|
||||
UIAFItem? uiaf = uiafEnumerator.Current;
|
||||
|
||||
if (entity is null && uiaf is not null)
|
||||
{
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
|
||||
add++;
|
||||
continue;
|
||||
}
|
||||
else if (entity is not null && uiaf is null)
|
||||
{
|
||||
// skip
|
||||
continue;
|
||||
}
|
||||
(EntityAchievement? entity, UIAFItem? uiaf) = enumerator.Current;
|
||||
|
||||
switch (entity, uiaf)
|
||||
{
|
||||
case (null, not null):
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
|
||||
add++;
|
||||
continue;
|
||||
case (not null, null):
|
||||
continue; // Skipped
|
||||
default:
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(uiaf);
|
||||
|
||||
if (entity.Id < uiaf.Id)
|
||||
switch (entity.Id.CompareTo(uiaf.Id))
|
||||
{
|
||||
moveEntity = true;
|
||||
moveUIAF = false;
|
||||
}
|
||||
else if (entity.Id == uiaf.Id)
|
||||
{
|
||||
moveEntity = true;
|
||||
moveUIAF = true;
|
||||
case < 0:
|
||||
(moveEntity, moveUIAF) = (true, false);
|
||||
break;
|
||||
case 0:
|
||||
(moveEntity, moveUIAF) = (true, true);
|
||||
|
||||
if (aggressive)
|
||||
{
|
||||
appDbContext.Achievements.RemoveAndSave(entity);
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
|
||||
update++;
|
||||
}
|
||||
|
||||
break;
|
||||
case > 0:
|
||||
(moveEntity, moveUIAF) = (false, true);
|
||||
|
||||
if (aggressive)
|
||||
{
|
||||
appDbContext.Achievements.RemoveAndSave(entity);
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
|
||||
update++;
|
||||
}
|
||||
add++;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// entity.Id > uiaf.Id
|
||||
moveEntity = false;
|
||||
moveUIAF = true;
|
||||
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
|
||||
add++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("{Method} Operation Complete, Add: {Add}, Update: {Update}", nameof(Merge), add, update);
|
||||
logger.LogInformation("Merge operation complete, [Add: {Add}], [Update: {Update}]", add, update);
|
||||
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)
|
||||
{
|
||||
logger.LogInformation("Perform {Method} Operation for archive: {Id}", nameof(Overwrite), archiveId);
|
||||
logger.LogInformation("Perform Overwrite Operation for [Archive: {Id}]", archiveId);
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
AppDbContext appDbContext = scope.GetAppDbContext();
|
||||
|
||||
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
|
||||
.AsNoTracking()
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.OrderBy(a => a.Id);
|
||||
|
||||
int add = 0;
|
||||
int update = 0;
|
||||
int remove = 0;
|
||||
(int add, int update, int remove) = (0, 0, 0);
|
||||
|
||||
using (IEnumerator<EntityAchievement> oldDataEnumerator = oldData.GetEnumerator())
|
||||
using (TwoEnumerbleEnumerator<EntityAchievement, EntityAchievement> enumerator = new(oldData, items))
|
||||
{
|
||||
using (IEnumerator<EntityAchievement> newDataEnumerator = items.GetEnumerator())
|
||||
(bool moveOld, bool moveNew) = (true, true);
|
||||
|
||||
while (true)
|
||||
{
|
||||
bool moveOld = true;
|
||||
bool moveNew = true;
|
||||
|
||||
while (true)
|
||||
if (!enumerator.MoveNext(ref moveOld, ref moveNew))
|
||||
{
|
||||
bool moveOldResult = moveOld && oldDataEnumerator.MoveNext();
|
||||
bool moveNewResult = moveNew && newDataEnumerator.MoveNext();
|
||||
break;
|
||||
}
|
||||
|
||||
if (moveOldResult || moveNewResult)
|
||||
{
|
||||
EntityAchievement? oldEntity = oldDataEnumerator.Current;
|
||||
EntityAchievement? newEntity = newDataEnumerator.Current;
|
||||
|
||||
if (oldEntity is null && newEntity is not null)
|
||||
{
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
add++;
|
||||
continue;
|
||||
}
|
||||
else if (oldEntity is not null && newEntity is null)
|
||||
{
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
remove++;
|
||||
continue;
|
||||
}
|
||||
(EntityAchievement? oldEntity, EntityAchievement? newEntity) = enumerator.Current;
|
||||
|
||||
switch (oldEntity, newEntity)
|
||||
{
|
||||
case (null, not null):
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
add++;
|
||||
continue;
|
||||
case (not null, null):
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
remove++;
|
||||
continue;
|
||||
default:
|
||||
ArgumentNullException.ThrowIfNull(oldEntity);
|
||||
ArgumentNullException.ThrowIfNull(newEntity);
|
||||
|
||||
if (oldEntity.Id < newEntity.Id)
|
||||
switch (oldEntity.Id.CompareTo(newEntity.Id))
|
||||
{
|
||||
moveOld = true;
|
||||
moveNew = false;
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
remove++;
|
||||
}
|
||||
else if (oldEntity.Id == newEntity.Id)
|
||||
{
|
||||
moveOld = true;
|
||||
moveNew = true;
|
||||
case < 0:
|
||||
(moveOld, moveNew) = (true, false);
|
||||
break;
|
||||
case 0:
|
||||
(moveOld, moveNew) = (true, true);
|
||||
|
||||
if (oldEntity.Equals(newEntity))
|
||||
{
|
||||
// Skip same entry, reduce write operation.
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
update++;
|
||||
}
|
||||
|
||||
break;
|
||||
case > 0:
|
||||
(moveOld, moveNew) = (false, true);
|
||||
|
||||
if (oldEntity.Equals(newEntity))
|
||||
{
|
||||
// skip same entry.
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
update++;
|
||||
}
|
||||
add++;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// entity.Id > uiaf.Id
|
||||
moveOld = false;
|
||||
moveNew = true;
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
add++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("{Method} Operation Complete, Add: {Add}, Update: {Update}, Remove: {Remove}", nameof(Overwrite), add, update, remove);
|
||||
logger.LogInformation("Overwrite Operation Complete, Add: {Add}, Update: {Update}, Remove: {Remove}", add, update, remove);
|
||||
return new(add, update, remove);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,167 +2,106 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using System.Collections.ObjectModel;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
/// <summary>
|
||||
/// 成就数据库服务
|
||||
/// </summary>
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IAchievementDbService))]
|
||||
internal sealed partial class AchievementDbService : IAchievementDbService
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public IServiceProvider ServiceProvider { get => serviceProvider; }
|
||||
|
||||
public Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId)
|
||||
{
|
||||
Dictionary<AchievementId, EntityAchievement> entities;
|
||||
try
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
entities = appDbContext.Achievements
|
||||
.AsNoTracking()
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.ToDictionary(a => (AchievementId)a.Id);
|
||||
}
|
||||
return this.Query<EntityAchievement, Dictionary<AchievementId, EntityAchievement>>(query => query
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.ToDictionary(a => (AchievementId)a.Id));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
throw ThrowHelper.DatabaseCorrupted(SH.ServiceAchievementUserdataCorruptedInnerIdNotUnique, ex);
|
||||
throw HutaoException.UserdataCorrupted(SH.ServiceAchievementUserdataCorruptedAchievementIdNotUnique, ex);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
public async ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId)
|
||||
public ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await appDbContext.Achievements
|
||||
.AsNoTracking()
|
||||
return this.QueryAsync<EntityAchievement, int>(
|
||||
(query, token) => query
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
|
||||
.CountAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
.CountAsync(token),
|
||||
token);
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA1305")]
|
||||
public async ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take)
|
||||
public ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await appDbContext.Achievements
|
||||
.AsNoTracking()
|
||||
return this.QueryAsync<EntityAchievement, List<EntityAchievement>>(
|
||||
(query, token) => query
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
|
||||
.OrderByDescending(a => a.Time.ToString())
|
||||
.Take(take)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
.ToListAsync(token),
|
||||
token);
|
||||
}
|
||||
|
||||
public void OverwriteAchievement(EntityAchievement achievement)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
this.DeleteByInnerId(achievement);
|
||||
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
|
||||
{
|
||||
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)
|
||||
{
|
||||
appDbContext.Achievements.AddAndSave(achievement);
|
||||
}
|
||||
this.Add(achievement);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask OverwriteAchievementAsync(EntityAchievement achievement)
|
||||
public async ValueTask OverwriteAchievementAsync(EntityAchievement achievement, CancellationToken token = default)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
await this.DeleteByInnerIdAsync(achievement, token).ConfigureAwait(false);
|
||||
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
|
||||
{
|
||||
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)
|
||||
{
|
||||
await appDbContext.Achievements.AddAndSaveAsync(achievement).ConfigureAwait(false);
|
||||
}
|
||||
await this.AddAsync(achievement, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<AchievementArchive> GetAchievementArchiveCollection()
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
return this.ObservableCollection<AchievementArchive>();
|
||||
}
|
||||
|
||||
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive)
|
||||
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive, CancellationToken token = default)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// It will cascade deleted the achievements.
|
||||
await appDbContext.AchievementArchives.RemoveAndSaveAsync(archive).ConfigureAwait(false);
|
||||
}
|
||||
// It will cascade deleted the achievements.
|
||||
await this.DeleteAsync(archive, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public List<EntityAchievement> GetAchievementListByArchiveId(Guid 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];
|
||||
}
|
||||
return this.ListByArchiveId<EntityAchievement>(archiveId);
|
||||
}
|
||||
|
||||
public async ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId)
|
||||
public ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await appDbContext.Achievements
|
||||
.AsNoTracking()
|
||||
.Where(i => i.ArchiveId == archiveId)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
return this.ListByArchiveIdAsync<EntityAchievement>(archiveId, token);
|
||||
}
|
||||
|
||||
public List<AchievementArchive> GetAchievementArchiveList()
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
IQueryable<AchievementArchive> result = appDbContext.AchievementArchives.AsNoTracking();
|
||||
return [.. result];
|
||||
}
|
||||
return this.List<AchievementArchive>();
|
||||
}
|
||||
|
||||
public async ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync()
|
||||
public ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await appDbContext.AchievementArchives.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
return this.ListAsync<AchievementArchive>(token);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,16 @@
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.Achievement;
|
||||
using System.Collections.ObjectModel;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement;
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
/// <summary>
|
||||
/// 成就服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
|
||||
@@ -25,21 +24,127 @@ internal sealed partial class AchievementService : IAchievementService
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<AchievementView> GetAchievementViewList(AchievementArchive archive, List<MetadataAchievement> metadata)
|
||||
private ObservableCollection<AchievementArchive>? archiveCollection;
|
||||
|
||||
public AchievementArchive? CurrentArchive
|
||||
{
|
||||
get => dbCurrent.Current;
|
||||
set => dbCurrent.Current = value;
|
||||
}
|
||||
|
||||
public ObservableCollection<AchievementArchive> ArchiveCollection
|
||||
{
|
||||
get
|
||||
{
|
||||
if (archiveCollection is null)
|
||||
{
|
||||
archiveCollection = achievementDbService.GetAchievementArchiveCollection();
|
||||
CurrentArchive = archiveCollection.SelectedOrDefault();
|
||||
}
|
||||
|
||||
return archiveCollection;
|
||||
}
|
||||
}
|
||||
|
||||
public List<AchievementView> GetAchievementViewList(AchievementArchive archive, AchievementServiceMetadataContext context)
|
||||
{
|
||||
Dictionary<AchievementId, EntityAchievement> entities = achievementDbService.GetAchievementMapByArchiveId(archive.InnerId);
|
||||
|
||||
return metadata.SelectList(meta =>
|
||||
return context.Achievements.SelectList(meta =>
|
||||
{
|
||||
EntityAchievement entity = entities.GetValueOrDefault(meta.Id) ?? EntityAchievement.From(archive.InnerId, meta.Id);
|
||||
return new AchievementView(entity, meta);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SaveAchievement(AchievementView achievement)
|
||||
{
|
||||
achievementDbService.OverwriteAchievement(achievement.Entity);
|
||||
}
|
||||
|
||||
public async ValueTask<ArchiveAddResultKind> AddArchiveAsync(AchievementArchive newArchive)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newArchive.Name))
|
||||
{
|
||||
return ArchiveAddResultKind.InvalidName;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(archiveCollection);
|
||||
|
||||
if (archiveCollection.Any(a => a.Name == newArchive.Name))
|
||||
{
|
||||
return ArchiveAddResultKind.AlreadyExists;
|
||||
}
|
||||
|
||||
// Sync cache
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
archiveCollection.Add(newArchive);
|
||||
|
||||
// Sync database
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
CurrentArchive = newArchive;
|
||||
|
||||
return ArchiveAddResultKind.Added;
|
||||
}
|
||||
|
||||
public async ValueTask RemoveArchiveAsync(AchievementArchive archive)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(archiveCollection);
|
||||
|
||||
// Sync cache
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
archiveCollection.Remove(archive);
|
||||
|
||||
// Sync database
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
await achievementDbService.RemoveAchievementArchiveAsync(archive).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<ImportResult> ImportFromUIAFAsync(AchievementArchive archive, List<UIAFItem> list, ImportStrategyKind strategy)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
|
||||
Guid archiveId = archive.InnerId;
|
||||
|
||||
switch (strategy)
|
||||
{
|
||||
case ImportStrategyKind.AggressiveMerge:
|
||||
{
|
||||
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
|
||||
return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, true);
|
||||
}
|
||||
|
||||
case ImportStrategyKind.LazyMerge:
|
||||
{
|
||||
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
|
||||
return achievementDbBulkOperation.Merge(archiveId, orederedUIAF, false);
|
||||
}
|
||||
|
||||
case ImportStrategyKind.Overwrite:
|
||||
{
|
||||
IEnumerable<EntityAchievement> orederedUIAF = list
|
||||
.SelectList(uiaf => EntityAchievement.From(archiveId, uiaf))
|
||||
.SortBy(a => a.Id);
|
||||
return achievementDbBulkOperation.Overwrite(archiveId, orederedUIAF);
|
||||
}
|
||||
|
||||
default:
|
||||
throw HutaoException.NotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<UIAF> ExportToUIAFAsync(AchievementArchive archive)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
List<EntityAchievement> entities = await achievementDbService
|
||||
.GetAchievementListByArchiveIdAsync(archive.InnerId)
|
||||
.ConfigureAwait(false);
|
||||
List<UIAFItem> list = entities.SelectList(UIAFItem.From);
|
||||
|
||||
return new()
|
||||
{
|
||||
Info = UIAFInfo.From(runtimeOptions),
|
||||
List = list,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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!;
|
||||
}
|
||||
@@ -13,32 +13,34 @@ namespace Snap.Hutao.Service.Achievement;
|
||||
[Injection(InjectAs.Scoped, typeof(IAchievementStatisticsService))]
|
||||
internal sealed partial class AchievementStatisticsService : IAchievementStatisticsService
|
||||
{
|
||||
private const int AchievementCardTakeCount = 2;
|
||||
|
||||
private readonly IAchievementDbService achievementDbService;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(Dictionary<AchievementId, MetadataAchievement> achievementMap)
|
||||
public async ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(AchievementServiceMetadataContext context, CancellationToken token = default)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
|
||||
List<AchievementStatistics> results = [];
|
||||
foreach (AchievementArchive archive in await achievementDbService.GetAchievementArchiveListAsync().ConfigureAwait(false))
|
||||
foreach (AchievementArchive archive in await achievementDbService.GetAchievementArchiveListAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
int finishedCount = await achievementDbService
|
||||
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId)
|
||||
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
int totalCount = achievementMap.Count;
|
||||
int totalCount = context.IdAchievementMap.Count;
|
||||
|
||||
List<EntityAchievement> achievements = await achievementDbService
|
||||
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, 2)
|
||||
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, AchievementCardTakeCount, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
results.Add(new()
|
||||
{
|
||||
DisplayName = archive.Name,
|
||||
FinishDescription = AchievementStatistics.Format(finishedCount, totalCount, out _),
|
||||
Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])),
|
||||
Achievements = achievements.SelectList(entity => new AchievementView(entity, context.IdAchievementMap[entity.Id])),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Achievement;
|
||||
/// 存档添加结果
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal enum ArchiveAddResult
|
||||
internal enum ArchiveAddResultKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加成功
|
||||
@@ -2,32 +2,33 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using System.Collections.ObjectModel;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
internal interface IAchievementDbService
|
||||
internal interface IAchievementDbService : IAppDbService<Model.Entity.AchievementArchive>, IAppDbService<EntityAchievement>
|
||||
{
|
||||
ValueTask RemoveAchievementArchiveAsync(Model.Entity.AchievementArchive archive);
|
||||
ValueTask RemoveAchievementArchiveAsync(Model.Entity.AchievementArchive archive, CancellationToken token = default);
|
||||
|
||||
ObservableCollection<Model.Entity.AchievementArchive> GetAchievementArchiveCollection();
|
||||
|
||||
List<Model.Entity.AchievementArchive> GetAchievementArchiveList();
|
||||
|
||||
ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync();
|
||||
ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default);
|
||||
|
||||
List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId);
|
||||
|
||||
ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId);
|
||||
ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default);
|
||||
|
||||
Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId);
|
||||
|
||||
ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId);
|
||||
ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default);
|
||||
|
||||
ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take);
|
||||
ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default);
|
||||
|
||||
void OverwriteAchievement(EntityAchievement achievement);
|
||||
|
||||
ValueTask OverwriteAchievementAsync(EntityAchievement achievement);
|
||||
ValueTask OverwriteAchievementAsync(EntityAchievement achievement, CancellationToken token = default);
|
||||
}
|
||||
@@ -32,13 +32,7 @@ internal interface IAchievementService
|
||||
/// <returns>UIAF</returns>
|
||||
ValueTask<UIAF> ExportToUIAFAsync(EntityArchive selectedArchive);
|
||||
|
||||
/// <summary>
|
||||
/// 获取整合的成就
|
||||
/// </summary>
|
||||
/// <param name="archive">用户</param>
|
||||
/// <param name="metadata">元数据</param>
|
||||
/// <returns>整合的成就</returns>
|
||||
List<AchievementView> GetAchievementViewList(EntityArchive archive, List<MetadataAchievement> metadata);
|
||||
List<AchievementView> GetAchievementViewList(EntityArchive archive, AchievementServiceMetadataContext context);
|
||||
|
||||
/// <summary>
|
||||
/// 异步导入UIAF数据
|
||||
@@ -47,7 +41,7 @@ internal interface IAchievementService
|
||||
/// <param name="list">UIAF数据</param>
|
||||
/// <param name="strategy">选项</param>
|
||||
/// <returns>导入结果</returns>
|
||||
ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy);
|
||||
ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategyKind strategy);
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除存档
|
||||
@@ -67,5 +61,5 @@ internal interface IAchievementService
|
||||
/// </summary>
|
||||
/// <param name="newArchive">新存档</param>
|
||||
/// <returns>存档添加结果</returns>
|
||||
ValueTask<ArchiveAddResult> AddArchiveAsync(EntityArchive newArchive);
|
||||
ValueTask<ArchiveAddResultKind> AddArchiveAsync(EntityArchive newArchive);
|
||||
}
|
||||
@@ -9,5 +9,5 @@ namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
internal interface IAchievementStatisticsService
|
||||
{
|
||||
ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(Dictionary<AchievementId, MetadataAchievement> achievementMap);
|
||||
ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(AchievementServiceMetadataContext context, CancellationToken token = default);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Achievement;
|
||||
/// 导入策略
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal enum ImportStrategy
|
||||
internal enum ImportStrategyKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 贪婪合并
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
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.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using WebAnnouncement = Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement.Announcement;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
@@ -72,7 +73,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
// 将公告内容联入公告列表
|
||||
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
|
||||
{
|
||||
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
{
|
||||
contentMap.TryGetValue(item.AnnId, out string? rawContent);
|
||||
item.Content = rawContent ?? string.Empty;
|
||||
@@ -83,7 +84,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
|
||||
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
|
||||
{
|
||||
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
|
||||
{
|
||||
item.Subtitle = new StringBuilder(item.Subtitle)
|
||||
.Replace("\r<br>", string.Empty)
|
||||
@@ -99,12 +100,12 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
private static void AdjustAnnouncementTime(List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
|
||||
{
|
||||
// 活动公告
|
||||
List<Announcement> activities = announcementListWrappers
|
||||
List<WebAnnouncement> activities = announcementListWrappers
|
||||
.Single(wrapper => wrapper.TypeId == 1)
|
||||
.List;
|
||||
|
||||
// 更新公告
|
||||
Announcement versionUpdate = announcementListWrappers
|
||||
WebAnnouncement versionUpdate = announcementListWrappers
|
||||
.Single(wrapper => wrapper.TypeId == 2)
|
||||
.List
|
||||
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
|
||||
@@ -116,7 +117,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
|
||||
|
||||
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionMatch.Groups[1].ValueSpan, offset);
|
||||
|
||||
foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities))
|
||||
foreach (ref readonly WebAnnouncement announcement in CollectionsMarshal.AsSpan(activities))
|
||||
{
|
||||
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
|
||||
{
|
||||
@@ -4,7 +4,7 @@
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
namespace Snap.Hutao.Service.Announcement;
|
||||
|
||||
/// <summary>
|
||||
/// 公告服务
|
||||
@@ -88,7 +88,6 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
|
||||
if (currentBackgroundPathSet is not { Count: > 0 })
|
||||
{
|
||||
string backgroundFolder = runtimeOptions.GetDataFolderBackgroundFolder();
|
||||
Directory.CreateDirectory(backgroundFolder);
|
||||
|
||||
currentBackgroundPathSet = Directory
|
||||
.GetFiles(backgroundFolder, "*.*", SearchOption.AllDirectories)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Discord.GameSDK.ABI;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Unicode;
|
||||
|
||||
namespace Snap.Hutao.Service.Discord;
|
||||
@@ -158,7 +159,7 @@ internal static class DiscordController
|
||||
static unsafe void DebugWriteDiscordMessage(void* state, DiscordLogLevel logLevel, sbyte* ptr)
|
||||
{
|
||||
ReadOnlySpan<byte> utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
|
||||
string message = System.Text.Encoding.UTF8.GetString(utf8);
|
||||
string message = Encoding.UTF8.GetString(utf8);
|
||||
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[{logLevel}]:{message}");
|
||||
}
|
||||
}
|
||||
@@ -224,18 +225,18 @@ internal static class DiscordController
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void SetString(sbyte* reference, int length, string source)
|
||||
private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan<char> source)
|
||||
{
|
||||
Span<sbyte> sbytes = new(reference, length);
|
||||
sbytes.Clear();
|
||||
Utf8.FromUtf16(source.AsSpan(), MemoryMarshal.Cast<sbyte, byte>(sbytes), out _, out _);
|
||||
Span<byte> bytes = new(reference, length);
|
||||
bytes.Clear();
|
||||
Utf8.FromUtf16(source, bytes, out _, out _);
|
||||
}
|
||||
|
||||
private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan<byte> source)
|
||||
{
|
||||
Span<sbyte> sbytes = new(reference, length);
|
||||
sbytes.Clear();
|
||||
source.CopyTo(MemoryMarshal.Cast<sbyte, byte>(sbytes));
|
||||
Span<byte> bytes = new(reference, length);
|
||||
bytes.Clear();
|
||||
source.CopyTo(bytes);
|
||||
}
|
||||
|
||||
private struct DiscordAsyncAction
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Snap.Hutao.Service.Game.Configuration;
|
||||
[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))]
|
||||
internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService
|
||||
{
|
||||
private readonly IGameConfigurationFileService gameConfigurationFileService;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
|
||||
public ChannelOptions GetChannelOptions()
|
||||
@@ -22,6 +23,13 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
|
||||
|
||||
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileSystem.GameFileName);
|
||||
|
||||
if (!File.Exists(gameFileSystem.GameConfigFilePath))
|
||||
{
|
||||
// Try restore the configuration file if it does not exist
|
||||
// The configuration file may be deleted by a incompatible launcher
|
||||
gameConfigurationFileService.Restore(gameFileSystem.GameConfigFilePath);
|
||||
}
|
||||
|
||||
if (!File.Exists(gameFileSystem.GameConfigFilePath))
|
||||
{
|
||||
return ChannelOptions.ConfigurationFileNotFound(gameFileSystem.GameConfigFilePath);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Configuration;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IGameConfigurationFileService))]
|
||||
internal sealed partial class GameConfigurationFileService : IGameConfigurationFileService
|
||||
{
|
||||
private const string ConfigurationFileName = "config.ini";
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
|
||||
public void Backup(string source)
|
||||
{
|
||||
if (File.Exists(source))
|
||||
{
|
||||
string serverCacheFolder = runtimeOptions.GetDataFolderServerCacheFolder();
|
||||
File.Copy(source, Path.Combine(serverCacheFolder, ConfigurationFileName), true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Restore(string destination)
|
||||
{
|
||||
string serverCacheFolder = runtimeOptions.GetDataFolderServerCacheFolder();
|
||||
string source = Path.Combine(serverCacheFolder, ConfigurationFileName);
|
||||
if (File.Exists(source))
|
||||
{
|
||||
File.Copy(source, destination, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Configuration;
|
||||
|
||||
internal interface IGameConfigurationFileService
|
||||
{
|
||||
void Backup(string source);
|
||||
|
||||
void Restore(string destination);
|
||||
}
|
||||
@@ -36,4 +36,6 @@ internal sealed class GameFileSystem
|
||||
public string GameConfigFilePath { get => gameConfigFilePath ??= Path.Combine(GameDirectory, GameConstants.ConfigFileName); }
|
||||
|
||||
public string PCGameSDKFilePath { get => pcGameSDKFilePath ??= Path.Combine(GameDirectory, GameConstants.PCGameSDKFilePath); }
|
||||
|
||||
public string ScreenShotDirectory { get => Path.Combine(GameDirectory, "ScreenShot"); }
|
||||
}
|
||||
@@ -178,7 +178,16 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
[AllowNull]
|
||||
public NameValue<int> Monitor
|
||||
{
|
||||
get => GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[int.Parse(index, CultureInfo.InvariantCulture) - 1], Monitors[0]);
|
||||
get
|
||||
{
|
||||
return GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[RestrictIndex(Monitors, index)], Monitors[0]);
|
||||
|
||||
static int RestrictIndex(List<NameValue<int>> monitors, string index)
|
||||
{
|
||||
return Math.Clamp(int.Parse(index, CultureInfo.InvariantCulture) - 1, 0, monitors.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value is not null)
|
||||
|
||||
@@ -7,6 +7,7 @@ using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
@@ -42,6 +43,9 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
|
||||
return;
|
||||
}
|
||||
|
||||
// Backup config file, in order to prevent a incompatible launcher to delete it.
|
||||
context.ServiceProvider.GetRequiredService<IGameConfigurationFileService>().Backup(gameFileSystem.GameConfigFilePath);
|
||||
|
||||
await context.TaskContext.SwitchToMainThreadAsync();
|
||||
context.UpdateGamePathEntry();
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ internal sealed partial class PackageConverter
|
||||
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
|
||||
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
|
||||
|
||||
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
|
||||
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.GetDataFolderServerCacheFolder(), gameFolder, scatteredFilesUrl);
|
||||
|
||||
// Step 1
|
||||
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
|
||||
|
||||
@@ -22,10 +22,10 @@ internal readonly struct PackageConverterFileSystemContext
|
||||
public readonly string ScatteredFilesUrl;
|
||||
public readonly string PkgVersionUrl;
|
||||
|
||||
public PackageConverterFileSystemContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
|
||||
public PackageConverterFileSystemContext(bool isTargetOversea, string serverCacheFolder, string gameFolder, string scatteredFilesUrl)
|
||||
{
|
||||
GameFolder = gameFolder;
|
||||
ServerCacheFolder = Path.Combine(dataFolder, "ServerCache");
|
||||
ServerCacheFolder = serverCacheFolder;
|
||||
|
||||
string serverCacheOversea = Path.Combine(ServerCacheFolder, "Oversea");
|
||||
string serverCacheChinese = Path.Combine(ServerCacheFolder, "Chinese");
|
||||
|
||||
@@ -19,12 +19,12 @@ internal static class GameFpsAddress
|
||||
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext state, in RequiredGameModule requiredGameModule)
|
||||
{
|
||||
bool readOk = UnsafeReadModulesMemory(state.GameProcess, requiredGameModule, out VirtualMemory localMemory);
|
||||
HutaoException.ThrowIfNot(readOk, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
|
||||
HutaoException.ThrowIfNot(readOk, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);
|
||||
|
||||
using (localMemory)
|
||||
{
|
||||
int offset = IndexOfPattern(localMemory.AsSpan()[(int)requiredGameModule.UnityPlayer.Size..]);
|
||||
HutaoException.ThrowIfNot(offset >= 0, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
HutaoException.ThrowIfNot(offset >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
|
||||
byte* pLocalMemory = (byte*)localMemory.Pointer;
|
||||
ref readonly Module unityPlayer = ref requiredGameModule.UnityPlayer;
|
||||
@@ -76,7 +76,7 @@ internal static class GameFpsAddress
|
||||
{
|
||||
value = 0;
|
||||
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, ref value, out _);
|
||||
HutaoException.ThrowIfNot(result, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
|
||||
HutaoException.ThrowIfNot(result, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,10 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> UnlockAsync(CancellationToken token = default)
|
||||
{
|
||||
HutaoException.ThrowIfNot(context.IsUnlockerValid, HutaoExceptionKind.GameFpsUnlockingFailed, "This Unlocker is invalid");
|
||||
HutaoException.ThrowIfNot(context.IsUnlockerValid, "This Unlocker is invalid");
|
||||
(FindModuleResult result, RequiredGameModule gameModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleNoModuleFound);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound);
|
||||
|
||||
GameFpsAddress.UnsafeFindFpsAddress(context, gameModule);
|
||||
context.Report();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hutao.HutaoAsAService;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
internal interface IHutaoAsAService
|
||||
{
|
||||
ValueTask<ObservableCollection<Announcement>> GetHutaoAnnouncementCollectionAsync(CancellationToken token = default);
|
||||
ValueTask<ObservableCollection<Web.Hutao.HutaoAsAService.Announcement>> GetHutaoAnnouncementCollectionAsync(CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdAchievementSource
|
||||
{
|
||||
public Dictionary<AchievementId, Model.Metadata.Achievement.Achievement> IdAchievementMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataListAchievementSource
|
||||
{
|
||||
public List<Model.Metadata.Achievement.Achievement> Achievements { get; set; }
|
||||
}
|
||||
@@ -18,15 +18,20 @@ internal static class MetadataServiceContextExtension
|
||||
|
||||
// List
|
||||
{
|
||||
if (context is IMetadataListMaterialSource listMaterialSource)
|
||||
if (context is IMetadataListAchievementSource listAchievementSource)
|
||||
{
|
||||
listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false);
|
||||
listAchievementSource.Achievements = await metadataService.GetAchievementListAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataListGachaEventSource listGachaEventSource)
|
||||
{
|
||||
listGachaEventSource.GachaEvents = await metadataService.GetGachaEventListAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataListMaterialSource listMaterialSource)
|
||||
{
|
||||
listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Dictionary
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Service.Notification;
|
||||
|
||||
internal interface IInfoBarOptionsBuilder : IBuilder
|
||||
{
|
||||
public InfoBarOptions Options { get; }
|
||||
}
|
||||
@@ -11,5 +11,5 @@ internal interface IInfoBarService
|
||||
{
|
||||
ObservableCollection<InfoBar> Collection { get; }
|
||||
|
||||
void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay);
|
||||
void PrepareInfoBarAndShow(Action<IInfoBarOptionsBuilder> configure);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
|
||||
namespace Snap.Hutao.Service.Notification;
|
||||
|
||||
internal sealed class InfoBarOptions
|
||||
{
|
||||
public InfoBarSeverity Severity { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? Message { get; set; }
|
||||
|
||||
public object? Content { get; set; }
|
||||
|
||||
public ButtonBase? ActionButton { get; set; }
|
||||
|
||||
public int MilliSecondsDelay { get; set; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user