mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
refactor dailynote service and partial gacha service
This commit is contained in:
65
src/Snap.Hutao/Snap.Hutao/Core/Threading/AsyncBarrier.cs
Normal file
65
src/Snap.Hutao/Snap.Hutao/Core/Threading/AsyncBarrier.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// An asynchronous barrier that blocks the signaler until all other participants have signaled.
|
||||
/// </summary>
|
||||
internal class AsyncBarrier
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of participants being synchronized.
|
||||
/// </summary>
|
||||
private readonly int participantCount;
|
||||
|
||||
/// <summary>
|
||||
/// The set of participants who have reached the barrier, with their awaiters that can resume those participants.
|
||||
/// </summary>
|
||||
private readonly Stack<TaskCompletionSource> waiters;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncBarrier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="participants">The number of participants.</param>
|
||||
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<TaskCompletionSource>(participants - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a participant is ready, and returns a Task
|
||||
/// that completes when all other participants have also signaled ready.
|
||||
/// </summary>
|
||||
/// <returns>A Task, which will complete (or may already be completed) when the last participant calls this method.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ internal static class NumberExtension
|
||||
/// <param name="x">给定的整数</param>
|
||||
/// <returns>位数</returns>
|
||||
[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);
|
||||
|
||||
@@ -51,4 +51,27 @@ internal static class SpanExtension
|
||||
right = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 求平均值
|
||||
/// </summary>
|
||||
/// <param name="span">跨度</param>
|
||||
/// <returns>平均值</returns>
|
||||
public static byte Average(this in ReadOnlySpan<byte> 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));
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem,
|
||||
|
||||
private static string GetItemTypeStringByItemId(uint itemId)
|
||||
{
|
||||
return itemId.Place() switch
|
||||
return itemId.StringLength() switch
|
||||
{
|
||||
8U => SH.ModelInterchangeUIGFItemTypeAvatar,
|
||||
5U => SH.ModelInterchangeUIGFItemTypeWeapon,
|
||||
|
||||
@@ -54,10 +54,10 @@ internal sealed class GachaEvent
|
||||
/// <summary>
|
||||
/// 五星列表
|
||||
/// </summary>
|
||||
public List<uint> UpOrangeList { get; set; } = default!;
|
||||
public HashSet<uint> UpOrangeList { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 四星列表
|
||||
/// </summary>
|
||||
public List<uint> UpPurpleList { get; set; } = default!;
|
||||
public HashSet<uint> UpPurpleList { get; set; } = default!;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
@@ -12,7 +12,9 @@ internal interface ICultivationDbService
|
||||
ValueTask DeleteCultivateEntryByIdAsync(Guid entryId);
|
||||
|
||||
ValueTask DeleteCultivateItemRangeByEntryIdAsync(Guid entryId);
|
||||
|
||||
ValueTask DeleteCultivateProjectByIdAsync(Guid projectId);
|
||||
|
||||
ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId);
|
||||
|
||||
ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId);
|
||||
|
||||
@@ -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<AppDbContext>();
|
||||
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<AppDbContext>();
|
||||
await appDbContext.DailyNotes.AddAndSaveAsync(entry).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DeleteDailyNoteEntryByIdAsync(Guid entryId)
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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<AppDbContext>();
|
||||
await appDbContext.DailyNotes.UpdateAndSaveAsync(entry).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DailyNoteEntry> GetDailyNoteEntryList()
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return appDbContext.DailyNotes.AsNoTracking().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public List<DailyNoteEntry> GetDailyNoteEntryIncludeUserList()
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return appDbContext.DailyNotes.AsNoTracking().Include(n => n.User).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.DailyNote;
|
||||
/// 实时便笺通知器
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="entry">实时便笺入口</param>
|
||||
public DailyNoteNotifier(IServiceProvider serviceProvider, DailyNoteEntry entry)
|
||||
public DailyNoteNotificationOperation(IServiceProvider serviceProvider, DailyNoteEntry entry)
|
||||
{
|
||||
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
@@ -42,7 +42,7 @@ internal sealed class DailyNoteNotifier
|
||||
/// 异步通知
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
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<NotifyInfo> 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<IGameService>().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<IGameService>().IsGameRunning();
|
||||
}
|
||||
|
||||
private readonly struct NotifyInfo
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public List<NameValue<int>> 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),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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<UserRemovedMessage>
|
||||
{
|
||||
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<U
|
||||
public async Task AddDailyNoteAsync(UserAndUid role)
|
||||
{
|
||||
string roleUid = role.Uid.Value;
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
|
||||
if (!dailyNoteDbService.ContainsUid(roleUid))
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DailyNoteEntry newEntry = DailyNoteEntry.From(role);
|
||||
|
||||
if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid))
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await serviceProvider
|
||||
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
|
||||
.Create(PlayerUid.IsOversea(roleUid))
|
||||
.GetDailyNoteAsync(role)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (dailyNoteResponse.IsOk())
|
||||
{
|
||||
DailyNoteEntry newEntry = DailyNoteEntry.From(role);
|
||||
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
|
||||
.PickRequiredService<IGameRecordClient>(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<U
|
||||
{
|
||||
await RefreshDailyNotesAsync().ConfigureAwait(false);
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
List<DailyNoteEntry> entryList = appDbContext.DailyNotes.ToList();
|
||||
entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); });
|
||||
entries = new(entryList);
|
||||
}
|
||||
List<DailyNoteEntry> 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<U
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask RefreshDailyNotesAsync()
|
||||
{
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
foreach (DailyNoteEntry entry in dailyNoteDbService.GetDailyNoteEntryIncludeUserList())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await serviceProvider
|
||||
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
|
||||
.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<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
|
||||
.PickRequiredService<IGameRecordClient>(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<U
|
||||
public async Task RemoveDailyNoteAsync(DailyNoteEntry entry)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
entries!.Remove(entry);
|
||||
ArgumentNullException.ThrowIfNull(entries);
|
||||
entries.Remove(entry);
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entry.InnerId).ConfigureAwait(false);
|
||||
}
|
||||
await dailyNoteDbService.DeleteDailyNoteEntryByIdAsync(entry.InnerId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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<DailyNoteEntry> GetDailyNoteEntryIncludeUserList();
|
||||
|
||||
List<DailyNoteEntry> GetDailyNoteEntryList();
|
||||
|
||||
ValueTask UpdateDailyNoteEntryAsync(DailyNoteEntry entry);
|
||||
}
|
||||
@@ -14,29 +14,6 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
/// </summary>
|
||||
internal static class GachaStatisticsExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 求平均值
|
||||
/// </summary>
|
||||
/// <param name="span">跨度</param>
|
||||
/// <returns>平均值</returns>
|
||||
public static byte Average(this in Span<byte> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完成添加
|
||||
/// </summary>
|
||||
@@ -82,7 +59,7 @@ internal static class GachaStatisticsExtension
|
||||
[SuppressMessage("", "IDE0057")]
|
||||
private static Color GetColorByName(string name)
|
||||
{
|
||||
Span<byte> codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
|
||||
ReadOnlySpan<byte> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<GachaStatistics> CreateAsync(IOrderedQueryable<GachaItem> items, GachaLogServiceContext context)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
|
||||
List<HistoryWishBuilder> 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<Avatar, int> orangeAvatarCounter = new();
|
||||
Dictionary<Avatar, int> 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;
|
||||
}
|
||||
|
||||
@@ -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<AvatarId, Avatar> idAvatarMap;
|
||||
private readonly Dictionary<WeaponId, Weapon> idWeaponMap;
|
||||
private readonly HutaoStatisticsFactoryMetadataContext context;
|
||||
private readonly GachaEvent avatarEvent;
|
||||
private readonly GachaEvent avatarEvent2;
|
||||
private readonly GachaEvent weaponEvent;
|
||||
|
||||
public HutaoStatisticsFactory(Dictionary<AvatarId, Avatar> idAvatarMap, Dictionary<WeaponId, Weapon> idWeaponMap, List<GachaEvent> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AvatarId, Avatar> IdAvatarMap;
|
||||
public readonly Dictionary<WeaponId, Weapon> IdWeaponMap;
|
||||
public readonly List<GachaEvent> GachaEvents;
|
||||
|
||||
public HutaoStatisticsFactoryMetadataContext(Dictionary<AvatarId, Avatar> idAvatarMap, Dictionary<WeaponId, Weapon> idWeaponMap, List<GachaEvent> gachaEvents)
|
||||
{
|
||||
IdAvatarMap = idAvatarMap;
|
||||
IdWeaponMap = idWeaponMap;
|
||||
GachaEvents = gachaEvents;
|
||||
}
|
||||
}
|
||||
@@ -68,8 +68,8 @@ internal sealed class TypedWishSummaryBuilder
|
||||
string name,
|
||||
Func<GachaConfigType, bool> 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;
|
||||
|
||||
@@ -106,7 +106,7 @@ internal readonly struct GachaLogServiceContext
|
||||
/// <returns>名称星级</returns>
|
||||
public INameQuality GetNameQualityByItemId(uint id)
|
||||
{
|
||||
uint place = id.Place();
|
||||
uint place = id.StringLength();
|
||||
return place switch
|
||||
{
|
||||
8U => IdAvatarMap[id],
|
||||
|
||||
@@ -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<ValueResult<bool, HutaoStatistics>> GetCurrentEventStatisticsAsync(CancellationToken token = default)
|
||||
{
|
||||
IMetadataService metadataService = serviceProvider.GetRequiredService<IMetadataService>();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<WeaponId, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
List<GachaEvent> gachaEvents = await metadataService.GetGachaEventsAsync(token).ConfigureAwait(false);
|
||||
|
||||
Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
|
||||
if (response.IsOk())
|
||||
Response<GachaEventStatistics> response = await homaGachaLogClient.GetGachaEventStatisticsAsync(token).ConfigureAwait(false);
|
||||
if (response.IsOk())
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<WeaponId, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
List<GachaEvent> 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);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,6 @@
|
||||
<None Remove="Resource\Icon\UI_ItemIcon_201.png" />
|
||||
<None Remove="Resource\Icon\UI_ItemIcon_204.png" />
|
||||
<None Remove="Resource\Icon\UI_ItemIcon_210.png" />
|
||||
<None Remove="Resource\Icon\UI_ItemIcon_210_256.png" />
|
||||
<None Remove="Resource\Icon\UI_ItemIcon_220021.png" />
|
||||
<None Remove="Resource\Icon\UI_MarkCustom_TagMonster.png" />
|
||||
<None Remove="Resource\Icon\UI_MarkQuest_Events_Proce.png" />
|
||||
@@ -230,7 +229,6 @@
|
||||
<Content Include="Resource\Icon\UI_ItemIcon_201.png" />
|
||||
<Content Include="Resource\Icon\UI_ItemIcon_204.png" />
|
||||
<Content Include="Resource\Icon\UI_ItemIcon_210.png" />
|
||||
<Content Include="Resource\Icon\UI_ItemIcon_210_256.png" />
|
||||
<Content Include="Resource\Icon\UI_ItemIcon_220021.png" />
|
||||
<Content Include="Resource\Icon\UI_MarkCustom_TagMonster.png" />
|
||||
<Content Include="Resource\Icon\UI_MarkQuest_Events_Proce.png" />
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding Uid}"/>
|
||||
<Border Grid.Row="1" Style="{StaticResource BorderCardStyle}">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<Image Width="64" Source="ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png"/>
|
||||
<Image Width="64" Source="ms-appx:///Resource/Icon/UI_ItemIcon_210.png"/>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center">
|
||||
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png"/>
|
||||
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_210.png"/>
|
||||
<ProgressRing
|
||||
Width="40"
|
||||
Height="40"
|
||||
|
||||
13
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/OssImages.cs
Normal file
13
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/OssImages.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
internal static class OssImages
|
||||
{
|
||||
public const string UIMarkQuestEventsProce = "https://upload-bbs.miyoushe.com/upload/2023/07/26/184872832/4b9ba51caa13e5516d507d64c7234b0c_7636696921807680614.png";
|
||||
public const string UIItemIcon220021 = "https://upload-bbs.miyoushe.com/upload/2023/07/26/184872832/7cd02abbf20727ec4fd0dae777b65748_6274739470729303254.png";
|
||||
public const string UIItemIcon210 = "https://upload-bbs.miyoushe.com/upload/2023/07/26/184872832/75dbb40f46380efbdbc23bd3a251c83a_538508470291955954.png";
|
||||
public const string UIItemIcon204 = "https://upload-bbs.miyoushe.com/upload/2023/07/26/184872832/4a3cbdd778648b8aee5c4ebe9672c68b_5158859174407167834.png";
|
||||
public const string UIIconInteeExplore1 = "https://upload-bbs.miyoushe.com/upload/2023/07/26/184872832/830fcd53539be4aa7faa02391f5384c5_8996801294815343498.png";
|
||||
}
|
||||
Reference in New Issue
Block a user