This commit is contained in:
Lightczx
2023-07-30 16:30:56 +08:00
parent 93ee1a3386
commit 2c45274cd3
40 changed files with 214 additions and 145 deletions

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class CommandGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
public const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{

View File

@@ -19,6 +19,12 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
{
"System.Threading.CancellationToken",
"System.Guid"
}.ToImmutableHashSet();
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
@@ -51,7 +57,6 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
};
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
@@ -102,6 +107,12 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
return;
}
// ICommand can only use Task or Task<T>
if (methodSymbol.GetAttributes().Any(attr=>attr.AttributeClass!.ToDisplayString() == Automation.CommandGenerator.AttributeName))
{
return;
}
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
{
Location location = methodSyntax.ReturnType.GetLocation();
@@ -113,7 +124,15 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
private static void HandleMethodParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
{
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
// 跳过方法定义 如 接口
if (methodSyntax.Body == null)
{
return;
}
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
{
return;
@@ -124,22 +143,19 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
return;
}
// 跳过异步方法,因为异步方法无法使用 ref in out
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword)))
foreach (SyntaxToken token in methodSyntax.Modifiers)
{
return;
}
// 跳过异步方法,因为异步方法无法使用 ref/in/out
if (token.IsKind(SyntaxKind.AsyncKeyword))
{
return;
}
// 跳过重载方法
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
{
return;
}
// 跳过方法定义 如 接口
if (methodSyntax.Body == null)
{
return;
// 跳过重载方法
if (token.IsKind(SyntaxKind.OverrideKeyword))
{
return;
}
}
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
@@ -151,8 +167,7 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
continue;
}
// 跳过 CancellationToken
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
if (RefLikeKeySkipTypes.Contains(symbol.Type.ToDisplayString()))
{
continue;
}

View File

@@ -23,7 +23,7 @@ internal static class ThrowHelper
/// <exception cref="OperationCanceledException">操作取消异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner)
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)
{
throw new OperationCanceledException(message, inner);
}
@@ -37,7 +37,7 @@ internal static class ThrowHelper
/// <exception cref="InvalidOperationException">无效操作异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidOperationException InvalidOperation(string message, Exception? inner)
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)
{
throw new InvalidOperationException(message, inner);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
internal static class DirectoryOperation
{
public static bool Move(string sourceDirName, string destDirName)
{
if (!Directory.Exists(sourceDirName))
{
return false;
}
Directory.Move(sourceDirName, destDirName);
return true;
}
}

View File

@@ -11,8 +11,15 @@ internal readonly struct Delay
/// <param name="minMilliSeconds">最小,闭</param>
/// <param name="maxMilliSeconds">最小,开</param>
/// <returns>任务</returns>
public static ValueTask RandomAsync(int minMilliSeconds, int maxMilliSeconds)
[SuppressMessage("", "VSTHRD200")]
public static ValueTask Random(int minMilliSeconds, int maxMilliSeconds)
{
return Task.Delay((int)(Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
return Task.Delay((int)(System.Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
}
[SuppressMessage("", "VSTHRD200")]
public static ValueTask FromSeconds(int seconds)
{
return Task.Delay(TimeSpan.FromSeconds(seconds)).AsValueTask();
}
}

View File

@@ -7,7 +7,6 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 元素类型
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1602")]
internal enum ElementType
{
None = 0, // 无元素

View File

@@ -7,7 +7,6 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 生长曲线
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1602")]
internal enum GrowCurveType
{
GROW_CURVE_NONE = 0,

View File

@@ -6,7 +6,6 @@ namespace Snap.Hutao.Model.Intrinsic;
/// <summary>
/// 材料类型
/// </summary>
[SuppressMessage("", "SA1602")]
internal enum MaterialType
{
MATERIAL_NONE = 0,

View File

@@ -7,7 +7,6 @@ namespace Snap.Hutao.Model.Intrinsic;
/// 怪物种类
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1602")]
internal enum MonsterType
{
MONSTER_NONE = 0,

View File

@@ -13,9 +13,4 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
/// </summary>
internal sealed class AvatarBaseValue : BaseValue
{
public PropertyCurveValue GetPropertyCurveValue(FightProperty fightProperty)
{
// TODO: impl
return default!;
}
}

View File

@@ -9,7 +9,6 @@ namespace Snap.Hutao.Model.Metadata;
/// 角色ID
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1600")]
internal static class AvatarIds
{
// 此处的变量名称以 UI_AvatarIcon 为准

View File

@@ -1410,6 +1410,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 UIGF 文件的语言:{0} 与胡桃的语言:{1} 不匹配,请切换到对应语言重试 的本地化字符串。
/// </summary>
internal static string ServiceGachaUIGFImportLanguageNotMatch {
get {
return ResourceManager.GetString("ServiceGachaUIGFImportLanguageNotMatch", resourceCulture);
}
}
/// <summary>
/// 查找类似 存在多个匹配账号,请删除重复的账号 的本地化字符串。
/// </summary>

View File

@@ -2075,4 +2075,7 @@
<data name="CoreExceptionServiceDatabaseCorruptedMessage" xml:space="preserve">
<value>数据库已损坏:{0}</value>
</data>
<data name="ServiceGachaUIGFImportLanguageNotMatch" xml:space="preserve">
<value>UIGF 文件的语言:{0} 与胡桃的语言:{1} 不匹配,请切换到对应语言重试</value>
</data>
</root>

View File

@@ -28,7 +28,6 @@ internal sealed partial class AchievementDbBulkOperation
/// <param name="items">待合并的项</param>
/// <param name="aggressive">是否贪婪</param>
/// <returns>导入结果</returns>
[SuppressMessage("", "SH002")]
public ImportResult Merge(Guid archiveId, IEnumerable<UIAFItem> items, bool aggressive)
{
logger.LogInformation("Perform {Method} Operation for archive: {Id}, Aggressive: {Aggressive}", nameof(Merge), archiveId, aggressive);
@@ -119,7 +118,6 @@ internal sealed partial class AchievementDbBulkOperation
/// <param name="archiveId">成就id</param>
/// <param name="items">待覆盖的项</param>
/// <returns>导入结果</returns>
[SuppressMessage("", "SH002")]
public ImportResult Overwrite(Guid archiveId, IEnumerable<EntityAchievement> items)
{
logger.LogInformation("Perform {Method} Operation for archive: {Id}", nameof(Overwrite), archiveId);

View File

@@ -15,39 +15,34 @@ namespace Snap.Hutao.Service.Achievement;
internal sealed partial class AchievementStatisticsService : IAchievementStatisticsService
{
private readonly IAchievementDbService achievementDbService;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async Task<List<AchievementStatistics>> GetAchievementStatisticsAsync(Dictionary<AchievementId, MetadataAchievement> achievementMap)
public async ValueTask<List<AchievementStatistics>> GetAchievementStatisticsAsync(Dictionary<AchievementId, MetadataAchievement> achievementMap)
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
List<AchievementStatistics> results = new();
foreach (AchievementArchive archive in achievementDbService.GetAchievementArchiveList())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
int finishedCount = await achievementDbService
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
List<AchievementStatistics> results = new();
foreach (AchievementArchive archive in achievementDbService.GetAchievementArchiveList())
int totalCount = achievementMap.Count;
List<EntityAchievement> achievements = await achievementDbService
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, 2)
.ConfigureAwait(false);
results.Add(new()
{
int finishedCount = await achievementDbService
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
int totalCount = achievementMap.Count;
List<EntityAchievement> achievements = await achievementDbService
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, 2)
.ConfigureAwait(false);
results.Add(new()
{
DisplayName = archive.Name,
FinishDescription = AchievementStatistics.Format(finishedCount, totalCount, out _),
Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])),
});
}
return results;
DisplayName = archive.Name,
FinishDescription = AchievementStatistics.Format(finishedCount, totalCount, out _),
Achievements = achievements.SelectList(entity => new AchievementView(entity, achievementMap[entity.Id])),
});
}
return results;
}
}

View File

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

View File

@@ -43,7 +43,7 @@ internal sealed partial class CultivationService
}
/// <inheritdoc/>
public async Task<ProjectAddResult> TryAddProjectAsync(CultivateProject project)
public async ValueTask<ProjectAddResult> TryAddProjectAsync(CultivateProject project)
{
if (string.IsNullOrWhiteSpace(project.Name))
{
@@ -69,7 +69,7 @@ internal sealed partial class CultivationService
}
/// <inheritdoc/>
public async Task RemoveProjectAsync(CultivateProject project)
public async ValueTask RemoveProjectAsync(CultivateProject project)
{
ArgumentNullException.ThrowIfNull(projects);

View File

@@ -72,7 +72,7 @@ internal interface ICultivationService
/// </summary>
/// <param name="project">项目</param>
/// <returns>任务</returns>
Task RemoveProjectAsync(CultivateProject project);
ValueTask RemoveProjectAsync(CultivateProject project);
/// <summary>
/// 异步保存养成物品
@@ -100,5 +100,5 @@ internal interface ICultivationService
/// </summary>
/// <param name="project">项目</param>
/// <returns>添加操作的结果</returns>
Task<ProjectAddResult> TryAddProjectAsync(CultivateProject project);
ValueTask<ProjectAddResult> TryAddProjectAsync(CultivateProject project);
}

View File

@@ -41,7 +41,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
}
/// <inheritdoc/>
public async Task AddDailyNoteAsync(UserAndUid role)
public async ValueTask AddDailyNoteAsync(UserAndUid role)
{
string roleUid = role.Uid.Value;
@@ -69,7 +69,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
}
/// <inheritdoc/>
public async Task<ObservableCollection<DailyNoteEntry>> GetDailyNoteEntriesAsync()
public async ValueTask<ObservableCollection<DailyNoteEntry>> GetDailyNoteEntryCollectionAsync()
{
if (entries == null)
{
@@ -111,7 +111,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
}
/// <inheritdoc/>
public async Task RemoveDailyNoteAsync(DailyNoteEntry entry)
public async ValueTask RemoveDailyNoteAsync(DailyNoteEntry entry)
{
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(entries);

View File

@@ -18,13 +18,13 @@ internal interface IDailyNoteService
/// </summary>
/// <param name="role">角色</param>
/// <returns>任务</returns>
Task AddDailyNoteAsync(UserAndUid role);
ValueTask AddDailyNoteAsync(UserAndUid role);
/// <summary>
/// 异步获取实时便笺列表
/// </summary>
/// <returns>实时便笺列表</returns>
Task<ObservableCollection<DailyNoteEntry>> GetDailyNoteEntriesAsync();
ValueTask<ObservableCollection<DailyNoteEntry>> GetDailyNoteEntryCollectionAsync();
/// <summary>
/// 异步刷新实时便笺
@@ -37,5 +37,5 @@ internal interface IDailyNoteService
/// </summary>
/// <param name="entry">指定的实时便笺</param>
/// <returns>任务</returns>
Task RemoveDailyNoteAsync(DailyNoteEntry entry);
ValueTask RemoveDailyNoteAsync(DailyNoteEntry entry);
}

View File

@@ -48,7 +48,7 @@ internal sealed partial class GachaStatisticsSlimFactory : IGachaStatisticsSlimF
}
}
private static GachaStatisticsSlim CreateCore(GachaLogServiceMetadataContext context, List<GachaItem> items, string uid)
private static GachaStatisticsSlim CreateCore(in GachaLogServiceMetadataContext context, List<GachaItem> items, string uid)
{
int standardOrangeTracker = 0;
int standardPurpleTracker = 0;

View File

@@ -34,7 +34,7 @@ internal sealed class PullPrediction
this.distributionType = distributionType;
}
public async Task PredictAsync(AsyncBarrier barrier)
public async ValueTask PredictAsync(AsyncBarrier barrier)
{
await taskContext.SwitchToBackgroundAsync();
HomaGachaLogClient gachaLogClient = serviceProvider.GetRequiredService<HomaGachaLogClient>();

View File

@@ -144,7 +144,7 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaArchives.AddAndSaveAsync(archive);
await appDbContext.GachaArchives.AddAndSaveAsync(archive).ConfigureAwait(false);
}
}

View File

@@ -110,7 +110,7 @@ internal struct GachaLogFetchContext
/// </summary>
/// <param name="item">物品</param>
/// <returns>是否应添加</returns>
public bool ShouldAddItem(GachaLogItem item)
public readonly bool ShouldAddItem(GachaLogItem item)
{
return !isLazy || item.Id > DbEndId;
}
@@ -120,7 +120,7 @@ internal struct GachaLogFetchContext
/// </summary>
/// <param name="items">物品集合</param>
/// <returns>当前类型已经处理完成</returns>
public bool ItemsHaveReachEnd(List<GachaLogItem> items)
public readonly bool ItemsHaveReachEnd(List<GachaLogItem> items)
{
return CurrentTypeAddingCompleted || items.Count < GachaLogQueryOptions.Size;
}
@@ -139,7 +139,7 @@ internal struct GachaLogFetchContext
/// <summary>
/// 保存物品
/// </summary>
public void SaveItems()
public readonly void SaveItems()
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -167,7 +167,7 @@ internal struct GachaLogFetchContext
/// </summary>
/// <param name="progress">进度</param>
/// <param name="isAuthKeyTimeout">验证密钥是否过期</param>
public void Report(IProgress<GachaLogFetchStatus> progress, bool isAuthKeyTimeout = false)
public readonly void Report(IProgress<GachaLogFetchStatus> progress, bool isAuthKeyTimeout = false)
{
FetchStatus.AuthKeyTimeout = isAuthKeyTimeout;
progress.Report(FetchStatus);

View File

@@ -55,7 +55,7 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
}
/// <inheritdoc/>
public async Task<ValueResult<bool, GachaArchive?>> RetrieveGachaItemsAsync(string uid, CancellationToken token = default)
public async ValueTask<ValueResult<bool, GachaArchive?>> RetrieveGachaItemsAsync(string uid, CancellationToken token = default)
{
GachaArchive? archive = await gachaLogDbService
.GetGachaArchiveByUidAsync(uid, token)
@@ -81,13 +81,13 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> DeleteGachaItemsAsync(string uid, CancellationToken token = default)
public async ValueTask<ValueResult<bool, string>> DeleteGachaItemsAsync(string uid, CancellationToken token = default)
{
return await homaGachaLogClient.DeleteGachaItemsAsync(uid, token).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default)
public async ValueTask<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default)
{
Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
if (response.IsOk())
@@ -110,7 +110,7 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
return new(false, default!);
}
private async Task<EndIds?> GetEndIdsFromCloudAsync(string uid, CancellationToken token = default)
private async ValueTask<EndIds?> GetEndIdsFromCloudAsync(string uid, CancellationToken token = default)
{
Response<EndIds> resp = await homaGachaLogClient.GetEndIdsAsync(uid, token).ConfigureAwait(false);
return resp.IsOk() ? resp.Data : default;

View File

@@ -206,7 +206,7 @@ internal sealed partial class GachaLogService : IGachaLogService
break;
}
await Delay.RandomAsync(1000, 2000).ConfigureAwait(false);
await Delay.Random(1000, 2000).ConfigureAwait(false);
}
while (true);
@@ -217,7 +217,7 @@ internal sealed partial class GachaLogService : IGachaLogService
token.ThrowIfCancellationRequested();
fetchContext.SaveItems();
await Delay.RandomAsync(1000, 2000).ConfigureAwait(false);
await Delay.Random(1000, 2000).ConfigureAwait(false);
}
return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive);

View File

@@ -19,14 +19,14 @@ internal interface IGachaLogHutaoCloudService
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>是否删除成功</returns>
Task<ValueResult<bool, string>> DeleteGachaItemsAsync(string uid, CancellationToken token = default);
ValueTask<ValueResult<bool, string>> DeleteGachaItemsAsync(string uid, CancellationToken token = default);
/// <summary>
/// 异步获取祈愿统计信息
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>祈愿统计信息</returns>
Task<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default);
ValueTask<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default);
ValueTask<Response<List<GachaEntry>>> GetGachaEntriesAsync(CancellationToken token = default);
@@ -36,7 +36,7 @@ internal interface IGachaLogHutaoCloudService
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>是否获取成功</returns>
Task<ValueResult<bool, GachaArchive?>> RetrieveGachaItemsAsync(string uid, CancellationToken token = default);
ValueTask<ValueResult<bool, GachaArchive?>> RetrieveGachaItemsAsync(string uid, CancellationToken token = default);
/// <summary>
/// 异步上传祈愿记录

View File

@@ -42,7 +42,7 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
string message = string.Format(
SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale,
queryLanguageCode,
metadataOptions.LocaleName);
metadataOptions.LanguageCode);
return new(false, message);
}
}

View File

@@ -79,7 +79,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
string message = string.Format(
SH.ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale,
queryLanguageCode,
metadataOptions.LocaleName);
metadataOptions.LanguageCode);
return new(false, message);
}
}

View File

@@ -1,8 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.Metadata;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
@@ -15,6 +17,7 @@ namespace Snap.Hutao.Service.GachaLog;
internal sealed partial class UIGFImportService : IUIGFImportService
{
private readonly ILogger<UIGFImportService> logger;
private readonly MetadataOptions metadataOptions;
private readonly IServiceProvider serviceProvider;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
@@ -24,6 +27,12 @@ internal sealed partial class UIGFImportService : IUIGFImportService
{
await taskContext.SwitchToBackgroundAsync();
if (!metadataOptions.IsCurrentLocale(uigf.Info.Language))
{
string message = string.Format(SH.ServiceGachaUIGFImportLanguageNotMatch, uigf.Info.Language, metadataOptions.LanguageCode);
ThrowHelper.InvalidOperation(message, null);
}
GachaArchiveOperation.GetOrAdd(serviceProvider, uigf.Info.Uid, archives, out GachaArchive? archive);
Guid archiveId = archive.InnerId;

View File

@@ -184,7 +184,7 @@ internal sealed partial class GameService : IGameService
}
/// <inheritdoc/>
public async Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
string gamePath = appOptions.GamePath;
string gameFolder = Path.GetDirectoryName(gamePath)!;

View File

@@ -76,7 +76,7 @@ internal interface IGameService
/// <param name="launchScheme">目标启动方案</param>
/// <param name="progress">进度</param>
/// <returns>是否替换成功</returns>
Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
/// <summary>
/// 修改注册表中的账号信息

View File

@@ -76,7 +76,7 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
private static string Unescape(string str)
{
string hex4Result = UTF16Regex().Replace(str, @"\u$1");
string hex4Result = UTF16Regex().Replace(str, @"\u");
// 不包含中文
// Some one's folder might begin with 'u'
@@ -89,6 +89,6 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
return Regex.Unescape(hex4Result);
}
[GeneratedRegex("\\\\x([0-9a-f]{4})")]
[GeneratedRegex(@"\\x(?=[0-9a-f]{4})")]
private static partial Regex UTF16Regex();
}

View File

@@ -42,12 +42,12 @@ internal readonly struct ItemOperationInfo
/// </summary>
/// <param name="type">操作类型</param>
/// <param name="target">目标</param>
/// <param name="cache">缓存</param>
public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem cache)
/// <param name="moveTo">缓存</param>
public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem moveTo)
{
Type = type;
Target = target.RemoteName;
MoveTo = cache.RemoteName;
MoveTo = moveTo.RemoteName;
Md5 = target.Md5;
TotalBytes = target.FileSize;
}

View File

@@ -10,9 +10,9 @@ namespace Snap.Hutao.Service.Game.Package;
internal enum ItemOperationType
{
/// <summary>
/// 删除
/// 添加
/// </summary>
Remove = 0,
Add = 0,
/// <summary>
/// 替换
@@ -20,7 +20,7 @@ internal enum ItemOperationType
Replace = 1,
/// <summary>
/// 添加
/// 需要备份
/// </summary>
Add = 2,
Backup = 2,
}

View File

@@ -22,6 +22,8 @@ namespace Snap.Hutao.Service.Game.Package;
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class PackageConverter
{
private const string PackageVersion = "pkg_version";
private readonly IServiceProvider serviceProvider;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
@@ -37,15 +39,33 @@ internal sealed partial class PackageConverter
/// <returns>替换结果与资源</returns>
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageReplaceStatus> progress)
{
// 以 国服 => 国际 为例
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
// 获取本地对应 pkg_version 文件,转换为索引字典
//
// 2. 对比两者差异,
// 国际服有 & 国服没有的 为新增
// 国际服有 & 国服也有的 为替换
// 剩余国际服没有 & 国服有的 为备份
// 生成对应的操作信息项对比文件的尺寸与MD5
//
// 3. 根据操作信息项,提取其中需要下载的项进行缓存对比或下载
// 若缓存中文件的尺寸与MD5与操作信息项中的一致则直接跳过
// 每个文件下载后需要验证文件文件的尺寸与MD5
// 若出现下载失败的情况,终止转换进程,此时国服文件尚未替换
//
// 4. 全部资源下载完成后,根据操作信息项,进行文件替换
// 处理顺序:备份/替换/新增
// 替换操作等于 先备份国服文件,随后新增国际服文件
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
Uri pkgVersionUri = $"{scatteredFilesUrl}/pkg_version".ToUri();
Uri pkgVersionUri = $"{scatteredFilesUrl}/{PackageVersion}".ToUri();
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
Dictionary<string, VersionItem> remoteItems = await TryGetRemoteItemsAsync(pkgVersionUri).ConfigureAwait(false);
Dictionary<string, VersionItem> localItems = await TryGetLocalItemsAsync(gameFolder, direction).ConfigureAwait(false);
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUri).ConfigureAwait(false);
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder, direction).ConfigureAwait(false);
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).OrderBy(i => (int)i.Type);
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).OrderBy(i => i.Type);
return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
}
@@ -116,12 +136,13 @@ internal sealed partial class PackageConverter
{
if (local.TryGetValue(remoteName, out VersionItem? localItem))
{
if (remoteItem.Md5 != localItem.Md5)
if (!remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))
{
// 本地发现了同名且不同MD5的项需要替换为服务器上的项
// 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项
yield return new(ItemOperationType.Replace, remoteItem, localItem);
}
// 同名同MD5跳过
local.Remove(remoteName);
}
else
@@ -131,30 +152,28 @@ internal sealed partial class PackageConverter
}
}
foreach (ItemOperationInfo item in local.Select(kvp => new ItemOperationInfo(ItemOperationType.Remove, kvp.Value, kvp.Value)))
foreach ((_, VersionItem localItem) in local)
{
yield return item;
yield return new(ItemOperationType.Backup, localItem, localItem);
}
}
private static void RenameDataFolder(string gameFolder, ConvertDirection direction)
private static void TryRenameDataFolder(string gameFolder, ConvertDirection direction)
{
string yuanShenData = Path.Combine(gameFolder, YuanShenData);
string genshinImpactData = Path.Combine(gameFolder, GenshinImpactData);
if (direction == ConvertDirection.ChineseToOversea)
try
{
if (Directory.Exists(yuanShenData))
{
Directory.Move(yuanShenData, genshinImpactData);
}
_ = direction == ConvertDirection.ChineseToOversea
? DirectoryOperation.Move(yuanShenData, genshinImpactData)
: DirectoryOperation.Move(genshinImpactData, yuanShenData);
}
else
catch (IOException ex)
{
if (Directory.Exists(genshinImpactData))
{
Directory.Move(genshinImpactData, yuanShenData);
}
// Access to the path is denied.
// When user install the game in special folder like 'Program Files'
throw ThrowHelper.GameFileOperation(SH.ServiceGamePackageRenameDataFolderFailed, ex);
}
}
@@ -164,15 +183,15 @@ internal sealed partial class PackageConverter
File.Move(targetFullPath, cacheFilePath, true);
}
private async ValueTask<Dictionary<string, VersionItem>> TryGetLocalItemsAsync(string gameFolder, ConvertDirection direction)
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder, ConvertDirection direction)
{
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version")))
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
{
return await GetLocalVersionItemsAsync(localSteam, direction).ConfigureAwait(false);
}
}
private async ValueTask<Dictionary<string, VersionItem>> TryGetRemoteItemsAsync(Uri pkgVersionUri)
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(Uri pkgVersionUri)
{
try
{
@@ -190,20 +209,11 @@ internal sealed partial class PackageConverter
private async ValueTask<bool> ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
{
// 重命名 _Data 目录
try
{
RenameDataFolder(gameFolder, direction);
}
catch (IOException ex)
{
// Access to the path is denied.
// When user install the game in special folder like 'Program Files'
throw ThrowHelper.GameFileOperation(SH.ServiceGamePackageRenameDataFolderFailed, ex);
}
TryRenameDataFolder(gameFolder, direction);
// Cache folder
Core.RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<Core.RuntimeOptions>();
string cacheFolder = Path.Combine(hutaoOptions.DataFolder, "ServerCache");
Core.RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<Core.RuntimeOptions>();
string cacheFolder = Path.Combine(runtimeOptions.DataFolder, "ServerCache");
// 执行下载与移动操作
foreach (ItemOperationInfo info in operations)
@@ -216,7 +226,7 @@ internal sealed partial class PackageConverter
switch (info.Type)
{
case ItemOperationType.Remove:
case ItemOperationType.Backup:
MoveToCache(moveToFilePath, targetFilePath);
break;
case ItemOperationType.Replace:
@@ -232,7 +242,7 @@ internal sealed partial class PackageConverter
}
// 重新下载所有 *pkg_version 文件
await ReplacePackageVersionsAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
await ReplacePackageVersionFilesAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
return true;
}
@@ -269,7 +279,7 @@ internal sealed partial class PackageConverter
{
StreamCopyWorker<PackageReplaceStatus> streamCopyWorker = new(webStream, fileStream, bytesRead => new(info.Target, bytesRead, totalBytes));
await streamCopyWorker.CopyAsync(progress).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
fileStream.Position = 0;
string remoteMd5 = await MD5.HashAsync(fileStream).ConfigureAwait(false);
if (string.Equals(info.Md5, remoteMd5, StringComparison.OrdinalIgnoreCase))
{
@@ -283,7 +293,7 @@ internal sealed partial class PackageConverter
// We want to retry forever.
serviceProvider.GetRequiredService<IInfoBarService>().Error(ex);
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
await Delay.FromSeconds(2).ConfigureAwait(false);
}
}
}
@@ -291,7 +301,7 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask ReplacePackageVersionsAsync(string scatteredFilesUrl, string gameFolder)
private async ValueTask ReplacePackageVersionFilesAsync(string scatteredFilesUrl, string gameFolder)
{
foreach (string versionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version"))
{
@@ -340,14 +350,14 @@ internal sealed partial class PackageConverter
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } raw)
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } row)
{
if (string.IsNullOrEmpty(raw))
if (string.IsNullOrEmpty(row))
{
continue;
}
VersionItem item = JsonSerializer.Deserialize<VersionItem>(raw, options)!;
VersionItem item = JsonSerializer.Deserialize<VersionItem>(row, options)!;
string remoteName = item.RemoteName;

View File

@@ -103,6 +103,11 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
/// <returns>是否为当前语言名称</returns>
public bool IsCurrentLocale(string languageCode)
{
if (string.IsNullOrEmpty(languageCode))
{
return false;
}
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode);
return GetLocaleName(cultureInfo) == LocaleName;
}

View File

@@ -53,7 +53,7 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
{
await taskContext.SwitchToBackgroundAsync();
ObservableCollection<UserAndUid> roles = await userService.GetRoleCollectionAsync().ConfigureAwait(false);
ObservableCollection<DailyNoteEntry> entries = await dailyNoteService.GetDailyNoteEntriesAsync().ConfigureAwait(false);
ObservableCollection<DailyNoteEntry> entries = await dailyNoteService.GetDailyNoteEntryCollectionAsync().ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
UserAndUids = roles;

View File

@@ -45,7 +45,7 @@ internal sealed class DailyNoteViewModelSlim : Abstraction.ViewModelSlim<View.Pa
.ConfigureAwait(false);
ObservableCollection<DailyNoteEntry> entries = await ServiceProvider
.GetRequiredService<IDailyNoteService>()
.GetDailyNoteEntriesAsync()
.GetDailyNoteEntryCollectionAsync()
.ConfigureAwait(false);
// We have to create a copy here,

View File

@@ -362,9 +362,17 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
{
await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = await contentDialogFactory.CreateForIndeterminateProgressAsync(SH.ViewModelGachaLogImportProgress).ConfigureAwait(true);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
try
{
await gachaLogService.ImportFromUIGFAsync(uigf).ConfigureAwait(false);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
await gachaLogService.ImportFromUIGFAsync(uigf).ConfigureAwait(false);
}
}
catch (InvalidOperationException ex)
{
infoBarService.Error(ex);
return false;
}
infoBarService.Success(SH.ViewModelGachaLogImportComplete);