refactor dailynote service and partial gacha service

This commit is contained in:
Lightczx
2023-07-26 23:46:11 +08:00
parent 8525aeafac
commit e843c84374
24 changed files with 328 additions and 146 deletions

View 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;
}
}
}
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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],

View File

@@ -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);
}

View File

@@ -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" />

View File

@@ -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"

View File

@@ -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"

View 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";
}