diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/AsyncBarrier.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/AsyncBarrier.cs new file mode 100644 index 00000000..13dfab76 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/AsyncBarrier.cs @@ -0,0 +1,65 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.Threading; + +/// +/// An asynchronous barrier that blocks the signaler until all other participants have signaled. +/// +internal class AsyncBarrier +{ + /// + /// The number of participants being synchronized. + /// + private readonly int participantCount; + + /// + /// The set of participants who have reached the barrier, with their awaiters that can resume those participants. + /// + private readonly Stack waiters; + + /// + /// Initializes a new instance of the class. + /// + /// The number of participants. + public AsyncBarrier(int participants) + { + Requires.Range(participants > 0, nameof(participants)); + participantCount = participants; + + // Allocate the stack so no resizing is necessary. + // We don't need space for the last participant, since we never have to store it. + waiters = new Stack(participants - 1); + } + + /// + /// Signals that a participant is ready, and returns a Task + /// that completes when all other participants have also signaled ready. + /// + /// A Task, which will complete (or may already be completed) when the last participant calls this method. + public Task SignalAndWaitAsync() + { + lock (waiters) + { + if (waiters.Count + 1 == participantCount) + { + // This is the last one we were waiting for. + // Unleash everyone that preceded this one. + while (waiters.Count > 0) + { + _ = Task.Factory.StartNew(state => ((TaskCompletionSource)state!).SetResult(), waiters.Pop(), default, TaskCreationOptions.None, TaskScheduler.Default); + } + + // And allow this one to continue immediately. + return Task.CompletedTask; + } + else + { + // We need more folks. So suspend this caller. + TaskCompletionSource tcs = new(); + waiters.Push(tcs); + return tcs.Task; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtension.cs index 268a24ba..ee452223 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtension.cs @@ -17,7 +17,7 @@ internal static class NumberExtension /// 给定的整数 /// 位数 [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint Place(in this uint x) + public static uint StringLength(in this uint x) { // Benchmarked and compared as a most optimized solution return (uint)(MathF.Log10(x) + 1); diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs index eec2f361..18db77a0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/SpanExtension.cs @@ -51,4 +51,27 @@ internal static class SpanExtension right = default; return false; } + + /// + /// 求平均值 + /// + /// 跨度 + /// 平均值 + public static byte Average(this in ReadOnlySpan span) + { + if (span.Length == 0) + { + return 0; + } + + int sum = 0; + int count = 0; + foreach (ref readonly byte b in span) + { + sum += b; + count++; + } + + return unchecked((byte)(sum / count)); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs index 5538f97e..1f7df4fd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/GachaLog/UIGFItem.cs @@ -40,7 +40,7 @@ internal sealed class UIGFItem : GachaLogItem, IMappingFrom SH.ModelInterchangeUIGFItemTypeAvatar, 5U => SH.ModelInterchangeUIGFItemTypeWeapon, diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs index 3cbcc597..dac8a7a7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/GachaEvent.cs @@ -54,10 +54,10 @@ internal sealed class GachaEvent /// /// 五星列表 /// - public List UpOrangeList { get; set; } = default!; + public HashSet UpOrangeList { get; set; } = default!; /// /// 四星列表 /// - public List UpPurpleList { get; set; } = default!; + public HashSet UpPurpleList { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210.png index ce3e342c..636ce0a2 100644 Binary files a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210.png and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210_256.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210_256.png deleted file mode 100644 index 636ce0a2..00000000 Binary files a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_210_256.png and /dev/null differ diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs index 8717142c..22239f7e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs @@ -12,7 +12,9 @@ internal interface ICultivationDbService ValueTask DeleteCultivateEntryByIdAsync(Guid entryId); ValueTask DeleteCultivateItemRangeByEntryIdAsync(Guid entryId); + ValueTask DeleteCultivateProjectByIdAsync(Guid projectId); + ValueTask GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId); ValueTask> GetCultivateEntryListByProjectIdAsync(Guid projectId); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteDbService.cs new file mode 100644 index 00000000..68fa3d22 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteDbService.cs @@ -0,0 +1,80 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.DependencyInjection.Abstraction; +using Snap.Hutao.Message; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service.Notification; +using Snap.Hutao.Service.User; +using Snap.Hutao.ViewModel.User; +using Snap.Hutao.Web.Hoyolab; +using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; +using System.Collections.ObjectModel; +using WebDailyNote = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote; + +namespace Snap.Hutao.Service.DailyNote; + +[ConstructorGenerated] +[Injection(InjectAs.Singleton, typeof(IDailyNoteDbService))] +internal sealed partial class DailyNoteDbService : IDailyNoteDbService +{ + private readonly IServiceProvider serviceProvider; + + public bool ContainsUid(string uid) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + return appDbContext.DailyNotes.AsNoTracking().Any(n => n.Uid == uid); + } + } + + public async ValueTask AddDailyNoteEntryAsync(DailyNoteEntry entry) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.DailyNotes.AddAndSaveAsync(entry).ConfigureAwait(false); + } + } + + public async ValueTask DeleteDailyNoteEntryByIdAsync(Guid entryId) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entryId).ConfigureAwait(false); + } + } + + public async ValueTask UpdateDailyNoteEntryAsync(DailyNoteEntry entry) + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + await appDbContext.DailyNotes.UpdateAndSaveAsync(entry).ConfigureAwait(false); + } + } + + public List GetDailyNoteEntryList() + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + return appDbContext.DailyNotes.AsNoTracking().ToList(); + } + } + + public List GetDailyNoteEntryIncludeUserList() + { + using (IServiceScope scope = serviceProvider.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + return appDbContext.DailyNotes.AsNoTracking().Include(n => n.User).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs similarity index 90% rename from src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs rename to src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs index 4f8ec94b..d1890b35 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotificationOperation.cs @@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.DailyNote; /// 实时便笺通知器 /// [HighQuality] -internal sealed class DailyNoteNotifier +internal sealed class DailyNoteNotificationOperation { private const string ToastHeaderIdArgument = "DAILYNOTE"; private const string ToastAttributionUnknown = "Unknown"; @@ -31,7 +31,7 @@ internal sealed class DailyNoteNotifier /// /// 服务提供器 /// 实时便笺入口 - public DailyNoteNotifier(IServiceProvider serviceProvider, DailyNoteEntry entry) + public DailyNoteNotificationOperation(IServiceProvider serviceProvider, DailyNoteEntry entry) { taskContext = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; @@ -42,7 +42,7 @@ internal sealed class DailyNoteNotifier /// 异步通知 /// /// 任务 - public async ValueTask NotifyAsync() + public async ValueTask SendAsync() { if (entry.DailyNote == null) { @@ -131,6 +131,9 @@ internal sealed class DailyNoteNotifier private static void CheckNotifySuppressed(DailyNoteEntry entry, List notifyInfos) { + // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#adding-images + // Image limitation. + // NotifySuppressed judge if (entry.DailyNote!.CurrentResin >= entry.ResinNotifyThreshold) { @@ -138,7 +141,7 @@ internal sealed class DailyNoteNotifier { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierResin, - "ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png", + Web.Hoyolab.OssImages.UIItemIcon210, $"{entry.DailyNote.CurrentResin}", string.Format(SH.ServiceDailyNoteNotifierResinCurrent, entry.DailyNote.CurrentResin))); entry.ResinNotifySuppressed = true; @@ -155,7 +158,7 @@ internal sealed class DailyNoteNotifier { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierHomeCoin, - "ms-appx:///Resource/Icon/UI_ItemIcon_204.png", + Web.Hoyolab.OssImages.UIItemIcon204, $"{entry.DailyNote.CurrentHomeCoin}", string.Format(SH.ServiceDailyNoteNotifierHomeCoinCurrent, entry.DailyNote.CurrentHomeCoin))); entry.HomeCoinNotifySuppressed = true; @@ -172,7 +175,7 @@ internal sealed class DailyNoteNotifier { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierDailyTask, - "ms-appx:///Resource/Icon/UI_MarkQuest_Events_Proce.png", + Web.Hoyolab.OssImages.UIMarkQuestEventsProce, SH.ServiceDailyNoteNotifierDailyTaskHint, entry.DailyNote.ExtraTaskRewardDescription)); entry.DailyTaskNotifySuppressed = true; @@ -189,7 +192,7 @@ internal sealed class DailyNoteNotifier { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierTransformer, - "ms-appx:///Resource/Icon/UI_ItemIcon_220021.png", + Web.Hoyolab.OssImages.UIItemIcon220021, SH.ServiceDailyNoteNotifierTransformerAdaptiveHint, SH.ServiceDailyNoteNotifierTransformerHint)); entry.TransformerNotifySuppressed = true; @@ -206,7 +209,7 @@ internal sealed class DailyNoteNotifier { notifyInfos.Add(new( SH.ServiceDailyNoteNotifierExpedition, - Web.HutaoEndpoints.UIAvatarIconSideNone.ToString(), // TODO: embed this + Web.Hoyolab.OssImages.UIIconInteeExplore1, SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint, SH.ServiceDailyNoteNotifierExpeditionHint)); entry.ExpeditionNotifySuppressed = true; @@ -220,17 +223,8 @@ internal sealed class DailyNoteNotifier private bool ShouldSuppressPopup(DailyNoteOptions options) { - bool isGameRunning = serviceProvider.GetRequiredService().IsGameRunning(); - - if (options.IsSilentWhenPlayingGame && isGameRunning) - { - // Prevent notify when we are in game && silent mode. - return true; - } - else - { - return false; - } + // Prevent notify when we are in game && silent mode. + return options.IsSilentWhenPlayingGame && serviceProvider.GetRequiredService().IsGameRunning(); } private readonly struct NotifyInfo diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs index 549bd83e..0258e1c1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs @@ -15,6 +15,8 @@ namespace Snap.Hutao.Service.DailyNote; [Injection(InjectAs.Singleton)] internal sealed class DailyNoteOptions : DbStoreOptions { + private const int OneMinute = 60; + private readonly IServiceProvider serviceProvider; private readonly IScheduleTaskInterop scheduleTaskInterop; @@ -38,11 +40,11 @@ internal sealed class DailyNoteOptions : DbStoreOptions /// public List> RefreshTimes { get; } = new() { - new(SH.ViewModelDailyNoteRefreshTime4, 240), - new(SH.ViewModelDailyNoteRefreshTime8, 480), - new(SH.ViewModelDailyNoteRefreshTime30, 1800), - new(SH.ViewModelDailyNoteRefreshTime40, 2400), - new(SH.ViewModelDailyNoteRefreshTime60, 3600), + new(SH.ViewModelDailyNoteRefreshTime4, OneMinute * 4), + new(SH.ViewModelDailyNoteRefreshTime8, OneMinute * 8), + new(SH.ViewModelDailyNoteRefreshTime30, OneMinute * 30), + new(SH.ViewModelDailyNoteRefreshTime40, OneMinute * 40), + new(SH.ViewModelDailyNoteRefreshTime60, OneMinute * 60), }; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs index 6ce0995d..5309043e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.DependencyInjection.Abstraction; using Snap.Hutao.Message; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; @@ -26,6 +27,7 @@ namespace Snap.Hutao.Service.DailyNote; internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient { private readonly IServiceProvider serviceProvider; + private readonly IDailyNoteDbService dailyNoteDbService; private readonly IUserService userService; private readonly ITaskContext taskContext; @@ -42,30 +44,27 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient(); + DailyNoteEntry newEntry = DailyNoteEntry.From(role); - if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid)) + Web.Response.Response dailyNoteResponse = await serviceProvider + .GetRequiredService>() + .Create(PlayerUid.IsOversea(roleUid)) + .GetDailyNoteAsync(role) + .ConfigureAwait(false); + + if (dailyNoteResponse.IsOk()) { - DailyNoteEntry newEntry = DailyNoteEntry.From(role); - - Web.Response.Response dailyNoteResponse = await scope.ServiceProvider - .PickRequiredService(PlayerUid.IsOversea(roleUid)) - .GetDailyNoteAsync(role) - .ConfigureAwait(false); - - if (dailyNoteResponse.IsOk()) - { - newEntry.UpdateDailyNote(dailyNoteResponse.Data); - } - - newEntry.UserGameRole = userService.GetUserGameRoleByUid(roleUid); - await appDbContext.DailyNotes.AddAndSaveAsync(newEntry).ConfigureAwait(false); - - await taskContext.SwitchToMainThreadAsync(); - entries?.Add(newEntry); + newEntry.UpdateDailyNote(dailyNoteResponse.Data); } + + newEntry.UserGameRole = userService.GetUserGameRoleByUid(roleUid); + await dailyNoteDbService.AddDailyNoteEntryAsync(newEntry).ConfigureAwait(false); + + await taskContext.SwitchToMainThreadAsync(); + entries?.Add(newEntry); } } @@ -76,13 +75,9 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient(); - List entryList = appDbContext.DailyNotes.ToList(); - entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); }); - entries = new(entryList); - } + List entryList = dailyNoteDbService.GetDailyNoteEntryList(); + entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); }); + entries = new(entryList); } return entries; @@ -91,31 +86,26 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient public async ValueTask RefreshDailyNotesAsync() { - using (IServiceScope scope = serviceProvider.CreateScope()) + foreach (DailyNoteEntry entry in dailyNoteDbService.GetDailyNoteEntryIncludeUserList()) { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + Web.Response.Response dailyNoteResponse = await serviceProvider + .GetRequiredService>() + .Create(PlayerUid.IsOversea(entry.Uid)) + .GetDailyNoteAsync(new(entry.User, entry.Uid)) + .ConfigureAwait(false); - foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User)) + if (dailyNoteResponse.IsOk()) { - Web.Response.Response dailyNoteResponse = await scope.ServiceProvider - .PickRequiredService(PlayerUid.IsOversea(entry.Uid)) - .GetDailyNoteAsync(new(entry.User, entry.Uid)) - .ConfigureAwait(false); + WebDailyNote dailyNote = dailyNoteResponse.Data!; - if (dailyNoteResponse.IsOk()) - { - WebDailyNote dailyNote = dailyNoteResponse.Data!; + // cache + await taskContext.SwitchToMainThreadAsync(); + entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote); - // database - entry.UpdateDailyNote(dailyNote); - - // cache - await taskContext.SwitchToMainThreadAsync(); - entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote); - - await new DailyNoteNotifier(serviceProvider, entry).NotifyAsync().ConfigureAwait(false); - await appDbContext.DailyNotes.UpdateAndSaveAsync(entry).ConfigureAwait(false); - } + // database + await new DailyNoteNotificationOperation(serviceProvider, entry).SendAsync().ConfigureAwait(false); + entry.DailyNote = dailyNote; + await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false); } } } @@ -124,13 +114,10 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient(); - await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entry.InnerId).ConfigureAwait(false); - } + await dailyNoteDbService.DeleteDailyNoteEntryByIdAsync(entry.InnerId).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteDbService.cs new file mode 100644 index 00000000..1878f37d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/IDailyNoteDbService.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; + +namespace Snap.Hutao.Service.DailyNote; + +internal interface IDailyNoteDbService +{ + ValueTask AddDailyNoteEntryAsync(DailyNoteEntry entry); + + bool ContainsUid(string uid); + + ValueTask DeleteDailyNoteEntryByIdAsync(Guid entryId); + + List GetDailyNoteEntryIncludeUserList(); + + List GetDailyNoteEntryList(); + + ValueTask UpdateDailyNoteEntryAsync(DailyNoteEntry entry); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtension.cs index 46628523..cc34c7ef 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtension.cs @@ -14,29 +14,6 @@ namespace Snap.Hutao.Service.GachaLog.Factory; /// internal static class GachaStatisticsExtension { - /// - /// 求平均值 - /// - /// 跨度 - /// 平均值 - public static byte Average(this in Span span) - { - int sum = 0; - int count = 0; - foreach (ref readonly byte b in span) - { - sum += b; - count++; - } - - if (count == 0) - { - return 0; - } - - return unchecked((byte)(sum / count)); - } - /// /// 完成添加 /// @@ -82,7 +59,7 @@ internal static class GachaStatisticsExtension [SuppressMessage("", "IDE0057")] private static Color GetColorByName(string name) { - Span codes = MD5.HashData(Encoding.UTF8.GetBytes(name)); + ReadOnlySpan codes = MD5.HashData(Encoding.UTF8.GetBytes(name)); return Color.FromArgb(255, codes.Slice(0, 5).Average(), codes.Slice(5, 5).Average(), codes.Slice(10, 5).Average()); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs index 4e82867f..c5e8d203 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -22,11 +22,13 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory { private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; + private readonly ITaskContext taskContext; private readonly AppOptions options; /// public async Task CreateAsync(IOrderedQueryable items, GachaLogServiceContext context) { + await taskContext.SwitchToBackgroundAsync(); List gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false); List historyWishBuilders = gachaEvents.SelectList(g => new HistoryWishBuilder(g, context)); @@ -44,23 +46,18 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory serviceProvider, SH.ServiceGachaLogFactoryPermanentWishName, TypedWishSummaryBuilder.IsStandardWish, - Web.Hutao.GachaLog.GachaDistributionType.Standard, - 90, - 10); + Web.Hutao.GachaLog.GachaDistributionType.Standard); TypedWishSummaryBuilder avatarWishBuilder = new( serviceProvider, SH.ServiceGachaLogFactoryAvatarWishName, TypedWishSummaryBuilder.IsAvatarEventWish, - Web.Hutao.GachaLog.GachaDistributionType.AvatarEvent, - 90, - 10); + Web.Hutao.GachaLog.GachaDistributionType.AvatarEvent); TypedWishSummaryBuilder weaponWishBuilder = new( serviceProvider, SH.ServiceGachaLogFactoryWeaponWishName, TypedWishSummaryBuilder.IsWeaponEventWish, Web.Hutao.GachaLog.GachaDistributionType.WeaponEvent, - 80, - 10); + 80); Dictionary orangeAvatarCounter = new(); Dictionary purpleAvatarCounter = new(); @@ -77,8 +74,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory .Where(w => w.ConfigType == item.GachaType) .SingleOrDefault(w => w.From <= item.Time && w.To >= item.Time); - // It's an avatar - switch (item.ItemId.Place()) + switch (item.ItemId.StringLength()) { case 8U: { @@ -135,7 +131,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory } default: - // ItemId place not correct. + // ItemId string length not correct. ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaStatisticsFactoryItemIdInvalid, item.ItemId), null!); break; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs index 422266c8..506e4eb2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactory.cs @@ -1,12 +1,10 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Abstraction; -using Snap.Hutao.Model.Metadata.Avatar; -using Snap.Hutao.Model.Metadata.Weapon; -using Snap.Hutao.Model.Primitive; using Snap.Hutao.ViewModel.GachaLog; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; using Snap.Hutao.Web.Hutao.GachaLog; @@ -16,21 +14,19 @@ namespace Snap.Hutao.Service.GachaLog.Factory; internal sealed class HutaoStatisticsFactory { - private readonly Dictionary idAvatarMap; - private readonly Dictionary idWeaponMap; + private readonly HutaoStatisticsFactoryMetadataContext context; private readonly GachaEvent avatarEvent; private readonly GachaEvent avatarEvent2; private readonly GachaEvent weaponEvent; - public HutaoStatisticsFactory(Dictionary idAvatarMap, Dictionary idWeaponMap, List gachaEvents) + public HutaoStatisticsFactory(in HutaoStatisticsFactoryMetadataContext context) { - this.idAvatarMap = idAvatarMap; - this.idWeaponMap = idWeaponMap; + this.context = context; DateTimeOffset now = DateTimeOffset.Now; - avatarEvent = gachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.AvatarEventWish); - avatarEvent2 = gachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.AvatarEventWish2); - weaponEvent = gachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.WeaponEventWish); + avatarEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.AvatarEventWish); + avatarEvent2 = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.AvatarEventWish2); + weaponEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaConfigType.WeaponEventWish); } public HutaoStatistics Create(GachaEventStatistics raw) @@ -52,11 +48,11 @@ internal sealed class HutaoStatisticsFactory foreach (ref readonly ItemCount item in CollectionsMarshal.AsSpan(items)) { - IStatisticsItemSource source = item.Item.Place() switch + IStatisticsItemSource source = item.Item.StringLength() switch { - 8U => idAvatarMap[item.Item], - 5U => idWeaponMap[item.Item], - _ => throw Must.NeverHappen($"不支持的物品 Id:{item.Item}"), + 8U => context.IdAvatarMap[item.Item], + 5U => context.IdWeaponMap[item.Item], + _ => throw ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaStatisticsFactoryItemIdInvalid, item.Item), null!), }; StatisticsItem statisticsItem = source.ToStatisticsItem(unchecked((int)item.Count)); @@ -73,6 +69,7 @@ internal sealed class HutaoStatisticsFactory QualityType.QUALITY_BLUE => blueItems, _ => throw Must.NeverHappen("意外的物品等级"), }; + list.Add(statisticsItem); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactoryMetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactoryMetadataContext.cs new file mode 100644 index 00000000..9076274d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HutaoStatisticsFactoryMetadataContext.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Weapon; +using Snap.Hutao.Model.Primitive; + +namespace Snap.Hutao.Service.GachaLog.Factory; + +internal readonly struct HutaoStatisticsFactoryMetadataContext +{ + public readonly Dictionary IdAvatarMap; + public readonly Dictionary IdWeaponMap; + public readonly List GachaEvents; + + public HutaoStatisticsFactoryMetadataContext(Dictionary idAvatarMap, Dictionary idWeaponMap, List gachaEvents) + { + IdAvatarMap = idAvatarMap; + IdWeaponMap = idWeaponMap; + GachaEvents = gachaEvents; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs index 595b4652..051271bf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs @@ -68,8 +68,8 @@ internal sealed class TypedWishSummaryBuilder string name, Func typeEvaluator, Web.Hutao.GachaLog.GachaDistributionType distributionType, - int guaranteeOrangeThreshold, - int guaranteePurpleThreshold) + int guaranteeOrangeThreshold = 90, + int guaranteePurpleThreshold = 10) { this.serviceProvider = serviceProvider; this.name = name; @@ -171,6 +171,7 @@ internal sealed class TypedWishSummaryBuilder OrangeList = summaryItems, }; + // TODO: barrier all predictions. new PullPrediction(serviceProvider, summary, distributionType).PredictAsync().SafeForget(); return summary; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs index 03cc1665..02980689 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs @@ -106,7 +106,7 @@ internal readonly struct GachaLogServiceContext /// 名称星级 public INameQuality GetNameQualityByItemId(uint id) { - uint place = id.Place(); + uint place = id.StringLength(); return place switch { 8U => IdAvatarMap[id], diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs index 63fadd86..a71ae389 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/HutaoCloudService.cs @@ -8,6 +8,7 @@ using Snap.Hutao.Model.Metadata; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Weapon; using Snap.Hutao.Model.Primitive; +using Snap.Hutao.Service.GachaLog.Factory; using Snap.Hutao.Service.Metadata; using Snap.Hutao.ViewModel.GachaLog; using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; @@ -99,17 +100,19 @@ internal sealed partial class HutaoCloudService : IHutaoCloudService public async Task> GetCurrentEventStatisticsAsync(CancellationToken token = default) { IMetadataService metadataService = serviceProvider.GetRequiredService(); - if (await metadataService.InitializeAsync().ConfigureAwait(false)) - { - Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false); - Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false); - List gachaEvents = await metadataService.GetGachaEventsAsync(token).ConfigureAwait(false); - Response response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false); - if (response.IsOk()) + Response response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false); + if (response.IsOk()) + { + if (await metadataService.InitializeAsync().ConfigureAwait(false)) { + Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false); + Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false); + List gachaEvents = await metadataService.GetGachaEventsAsync(token).ConfigureAwait(false); + HutaoStatisticsFactoryMetadataContext context = new(idAvatarMap, idWeaponMap, gachaEvents); + GachaEventStatistics raw = response.Data; - Factory.HutaoStatisticsFactory factory = new(idAvatarMap, idWeaponMap, gachaEvents); + HutaoStatisticsFactory factory = new(context); HutaoStatistics statistics = factory.Create(raw); return new(true, statistics); } diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index afeec2e6..c24a6729 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -99,7 +99,6 @@ - @@ -230,7 +229,6 @@ - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Card/DailyNoteCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Card/DailyNoteCard.xaml index 9fbbae56..87d3ade6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Card/DailyNoteCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Card/DailyNoteCard.xaml @@ -48,7 +48,7 @@ - + - +