diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx index 3c70037..4190042 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx @@ -2453,6 +2453,9 @@ Space Available: {2} Go + + Sync All Characters and Weapons + Sync Inventory Items diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 1701d4e..9b55ffb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2504,6 +2504,9 @@ 前往 + + 同步所有角色与武器 + 同步背包物品 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx index d5d5541..17c10c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx @@ -2429,6 +2429,9 @@ 前往 + + 同步所有角色與武器 + 同步背包物品 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/AvatarPropertyBatchCultivateService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/AvatarPropertyBatchCultivateService.cs new file mode 100644 index 0000000..a86e938 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/AvatarPropertyBatchCultivateService.cs @@ -0,0 +1,179 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Model.Calculable; +using Snap.Hutao.Model.Cultivation; +using Snap.Hutao.Model.Entity.Primitive; +using Snap.Hutao.Service.AvatarInfo.Factory; +using Snap.Hutao.Service.Cultivation.Consumption; +using Snap.Hutao.Service.Cultivation.Offline; +using Snap.Hutao.UI.Xaml.View.Dialog; +using Snap.Hutao.ViewModel.AvatarProperty; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; +using System.Collections.Immutable; +using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta; +using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption; +using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption; +using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper; + +namespace Snap.Hutao.Service.Cultivation; + +[Service(ServiceLifetime.Singleton, typeof(IAvatarPropertyBatchCultivateService))] +internal sealed partial class AvatarPropertyBatchCultivateService : IAvatarPropertyBatchCultivateService +{ + private readonly IContentDialogFactory contentDialogFactory; + private readonly ICultivationService cultivationService; + private readonly IServiceProvider serviceProvider; + + [GeneratedConstructor] + public partial AvatarPropertyBatchCultivateService(IServiceProvider serviceProvider); + + public async ValueTask ExecuteAsync(SummaryFactoryMetadataContext metadataContext, ImmutableArray targetAvatars, CancellationToken cancellationToken) + { + CultivateProjectAvatarPropertyBatchPreferences? batchPrefs = await cultivationService + .GetAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync() + .ConfigureAwait(false); + + CultivatePromotionDeltaBatchDialog dialog = batchPrefs is null + ? await contentDialogFactory + .CreateInstanceAsync(serviceProvider) + .ConfigureAwait(false) + : await contentDialogFactory + .CreateInstanceAsync(serviceProvider, batchPrefs) + .ConfigureAwait(false); + + if (await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false) is not (true, { } baseline)) + { + return null; + } + + await cultivationService + .SaveAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync(ToAvatarPropertyBatchPreferences(baseline)) + .ConfigureAwait(false); + + ArgumentNullException.ThrowIfNull(baseline.Delta.Weapon); + + ContentDialog progressDialog = await contentDialogFactory + .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) + .ConfigureAwait(false); + + BatchCultivateResult result = default; + using (await contentDialogFactory.BlockAsync(progressDialog).ConfigureAwait(false)) + { + if (baseline.ClearAvatarAndWeaponEntriesBeforeSync) + { + await cultivationService.RemoveAvatarAndWeaponEntriesForCurrentProjectAsync().ConfigureAwait(false); + } + + ImmutableArray.Builder deltasBuilder = ImmutableArray.CreateBuilder(); + foreach (AvatarView avatar in targetAvatars) + { + if (!baseline.Delta.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy)) + { + ++result.SkippedCount; + continue; + } + + deltasBuilder.Add(copy); + } + + ImmutableArray deltas = deltasBuilder.ToImmutable(); + + CalculatorBatchConsumption batchConsumption = OfflineCalculator.CalculateBatchConsumption(deltas, metadataContext); + + foreach ((CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta) in batchConsumption.Items.Zip(deltas)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!await SaveCultivationAsync(consumption, new CultivatePromotionDeltaOptions(delta, baseline.Strategy)).ConfigureAwait(false)) + { + break; + } + + ++result.SucceedCount; + } + } + + return result; + } + + private static CultivateProjectAvatarPropertyBatchPreferences ToAvatarPropertyBatchPreferences(CultivatePromotionDeltaOptions baseline) + { + CalculatorAvatarPromotionDelta d = baseline.Delta; + uint sa = 10; + uint se = 10; + uint sq = 10; + if (d.SkillList is [{ } a, { } e, { } q, ..]) + { + sa = a.LevelTarget; + se = e.LevelTarget; + sq = q.LevelTarget; + } + + return new CultivateProjectAvatarPropertyBatchPreferences + { + AvatarLevelTarget = d.AvatarLevelTarget, + SkillATarget = sa, + SkillETarget = se, + SkillQTarget = sq, + WeaponLevelTarget = d.Weapon?.LevelTarget ?? 90U, + ConsumptionSaveStrategyIndex = (int)baseline.Strategy, + ClearAvatarAndWeaponEntriesBeforeSync = baseline.ClearAvatarAndWeaponEntriesBeforeSync, + }; + } + + private async ValueTask SaveCultivationAsync(CalculatorConsumption consumption, CultivatePromotionDeltaOptions options) + { + LevelInformation levelInformation = LevelInformation.From(options.Delta); + + InputConsumption avatarInput = new() + { + Type = CultivateType.AvatarAndSkill, + ItemId = options.Delta.AvatarId, + Items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume), + LevelInformation = levelInformation, + Strategy = options.Strategy, + }; + + ConsumptionSaveResult avatarSave = await cultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false); + + if (avatarSave.Kind is ConsumptionSaveResultKind.NoProject) + { + return false; + } + + ArgumentNullException.ThrowIfNull(options.Delta.Weapon); + + Guid? relatedAvatarEntryId = avatarSave.CreatedEntryInnerId; + if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.Skipped) + { + relatedAvatarEntryId = await cultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false); + } + + if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.NoItem) + { + relatedAvatarEntryId = await cultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false); + } + + if (relatedAvatarEntryId is null && !consumption.WeaponConsume.IsEmpty) + { + relatedAvatarEntryId = await cultivationService.EnsureAvatarAssociationStubAsync(options.Delta.AvatarId, levelInformation).ConfigureAwait(false); + } + + InputConsumption weaponInput = new() + { + Type = CultivateType.Weapon, + ItemId = options.Delta.Weapon.Id, + Items = consumption.WeaponConsume, + LevelInformation = levelInformation, + Strategy = options.Strategy, + RelatedEntryId = relatedAvatarEntryId, + }; + + ConsumptionSaveResult weaponSave = await cultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false); + + return weaponSave.Kind is not ConsumptionSaveResultKind.NoProject; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/BatchCultivateResult.cs similarity index 79% rename from src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Cultivation/BatchCultivateResult.cs index 266f052..9fea17f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/BatchCultivateResult.cs @@ -1,10 +1,10 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.ViewModel.AvatarProperty; +namespace Snap.Hutao.Service.Cultivation; internal struct BatchCultivateResult { public int SucceedCount; public int SkippedCount; -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/IAvatarPropertyBatchCultivateService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/IAvatarPropertyBatchCultivateService.cs new file mode 100644 index 0000000..c5b269c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/IAvatarPropertyBatchCultivateService.cs @@ -0,0 +1,16 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.AvatarInfo.Factory; +using Snap.Hutao.ViewModel.AvatarProperty; +using System.Collections.Immutable; + +namespace Snap.Hutao.Service.Cultivation; + +internal interface IAvatarPropertyBatchCultivateService +{ + /// + /// when the baseline dialog is dismissed without confirmation. + /// + ValueTask ExecuteAsync(SummaryFactoryMetadataContext metadataContext, ImmutableArray targetAvatars, CancellationToken cancellationToken); +} diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml index f32349c..2e87130 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml @@ -467,6 +467,10 @@ CommandParameter="{Binding Projects.CurrentItem, Mode=OneWay}" Icon="{shuxm:FontIcon Glyph=}" Label="{shuxm:ResourceString Name=ViewPageCultivationClearInventory}"/> + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index a2358dc..efc0584 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -16,6 +16,7 @@ using Snap.Hutao.Service.AvatarInfo; using Snap.Hutao.Service.AvatarInfo.Factory; using Snap.Hutao.Service.Cultivation; using Snap.Hutao.Service.Cultivation.Consumption; +using BatchCultivateResult = Snap.Hutao.Service.Cultivation.BatchCultivateResult; using Snap.Hutao.Service.Cultivation.Offline; using Snap.Hutao.Service.Metadata.ContextAbstraction; using Snap.Hutao.Service.Notification; @@ -28,7 +29,6 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; using System.Collections.Immutable; using System.Globalization; using System.Runtime.CompilerServices; -using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta; using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption; using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption; using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper; @@ -41,6 +41,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I { private readonly ExclusiveTokenProvider refreshTokenProvider = new(); private readonly AvatarPropertyViewModelScopeContext scopeContext; + private readonly IAvatarPropertyBatchCultivateService avatarPropertyBatchCultivateService; private SummaryFactoryMetadataContext? metadataContext; @@ -270,69 +271,16 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I targetAvatars = [.. avatars.Source]; } - CultivateProjectAvatarPropertyBatchPreferences? batchPrefs = await scopeContext.CultivationService - .GetAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync() + ArgumentNullException.ThrowIfNull(metadataContext); + BatchCultivateResult? batchResult = await avatarPropertyBatchCultivateService + .ExecuteAsync(metadataContext, targetAvatars, CancellationToken) .ConfigureAwait(false); - CultivatePromotionDeltaBatchDialog dialog = batchPrefs is null - ? await scopeContext.ContentDialogFactory - .CreateInstanceAsync(scopeContext.ServiceProvider) - .ConfigureAwait(false) - : await scopeContext.ContentDialogFactory - .CreateInstanceAsync(scopeContext.ServiceProvider, batchPrefs) - .ConfigureAwait(false); - - if (await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false) is not (true, { } baseline)) + if (batchResult is not { } result) { return; } - await scopeContext.CultivationService - .SaveAvatarPropertyBatchCultivatePreferencesForCurrentProjectAsync(ToAvatarPropertyBatchPreferences(baseline)) - .ConfigureAwait(false); - - ArgumentNullException.ThrowIfNull(baseline.Delta.Weapon); - - ContentDialog progressDialog = await scopeContext.ContentDialogFactory - .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) - .ConfigureAwait(false); - - BatchCultivateResult result = default; - using (await scopeContext.ContentDialogFactory.BlockAsync(progressDialog).ConfigureAwait(false)) - { - if (baseline.ClearAvatarAndWeaponEntriesBeforeSync) - { - await scopeContext.CultivationService.RemoveAvatarAndWeaponEntriesForCurrentProjectAsync().ConfigureAwait(false); - } - - ImmutableArray.Builder deltasBuilder = ImmutableArray.CreateBuilder(); - foreach (AvatarView avatar in targetAvatars) - { - if (!baseline.Delta.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy)) - { - ++result.SkippedCount; - continue; - } - - deltasBuilder.Add(copy); - } - - ImmutableArray deltas = deltasBuilder.ToImmutable(); - - ArgumentNullException.ThrowIfNull(metadataContext); - CalculatorBatchConsumption batchConsumption = OfflineCalculator.CalculateBatchConsumption(deltas, metadataContext); - - foreach ((CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta) in batchConsumption.Items.Zip(deltas)) - { - if (!await SaveCultivationAsync(consumption, new CultivatePromotionDeltaOptions(delta, baseline.Strategy), true).ConfigureAwait(false)) - { - break; - } - - ++result.SucceedCount; - } - } - scopeContext.Messenger.Send(CultivationProjectEntriesChangedMessage.Empty); InfoBarMessage message = result.SkippedCount > 0 @@ -342,31 +290,6 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I scopeContext.Messenger.Send(message); } - private static CultivateProjectAvatarPropertyBatchPreferences ToAvatarPropertyBatchPreferences(CultivatePromotionDeltaOptions baseline) - { - CalculatorAvatarPromotionDelta d = baseline.Delta; - uint sa = 10; - uint se = 10; - uint sq = 10; - if (d.SkillList is [{ } a, { } e, { } q, ..]) - { - sa = a.LevelTarget; - se = e.LevelTarget; - sq = q.LevelTarget; - } - - return new CultivateProjectAvatarPropertyBatchPreferences - { - AvatarLevelTarget = d.AvatarLevelTarget, - SkillATarget = sa, - SkillETarget = se, - SkillQTarget = sq, - WeaponLevelTarget = d.Weapon?.LevelTarget ?? 90U, - ConsumptionSaveStrategyIndex = (int)baseline.Strategy, - ClearAvatarAndWeaponEntriesBeforeSync = baseline.ClearAvatarAndWeaponEntriesBeforeSync, - }; - } - /// if we can continue saving consumptions, otherwise . private async ValueTask SaveCultivationAsync(CalculatorConsumption consumption, CultivatePromotionDeltaOptions options, bool isBatch = false) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs index e4d79e0..8c16b39 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivationViewModel.cs @@ -11,19 +11,24 @@ using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Setting; using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.AvatarInfo; +using Snap.Hutao.Service.AvatarInfo.Factory; using Snap.Hutao.Service.Cultivation; using Snap.Hutao.Service.Inventory; using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Metadata.ContextAbstraction; using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Notification; +using Snap.Hutao.Service.User; using Snap.Hutao.Service.Yae; +using Snap.Hutao.ViewModel.AvatarProperty; using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox; using Snap.Hutao.UI.Xaml.Data; using Snap.Hutao.UI.Xaml.View.Dialog; using Snap.Hutao.ViewModel.Game; using System.Collections.Immutable; using System.Collections.ObjectModel; +using BatchCultivateResult = Snap.Hutao.Service.Cultivation.BatchCultivateResult; namespace Snap.Hutao.ViewModel.Cultivation; @@ -38,6 +43,9 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel, IRec private readonly ICultivationService cultivationService; private readonly INavigationService navigationService; private readonly IInventoryService inventoryService; + private readonly IAvatarInfoService avatarInfoService; + private readonly IAvatarPropertyBatchCultivateService avatarPropertyBatchCultivateService; + private readonly IUserService userService; private readonly IServiceProvider serviceProvider; private readonly IMetadataService metadataService; private readonly ITaskContext taskContext; @@ -322,6 +330,57 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel, IRec } } + [Command("SyncAllAvatarsAndWeaponsCommand")] + private async Task SyncAllAvatarsAndWeaponsAsync() + { + SentrySdk.AddBreadcrumb(BreadcrumbFactory2.CreateUI("Sync all avatars and weapons to cultivation", "CultivationViewModel.Command", [])); + + if (Projects?.CurrentItem is null) + { + return; + } + + if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid) + { + messenger.Send(InfoBarMessage.Warning(SH.MustSelectUserAndUid)); + return; + } + + SummaryFactoryMetadataContext summaryContext = await metadataService.GetContextAsync(CancellationToken).ConfigureAwait(false); + Summary? summary = await avatarInfoService.GetSummaryAsync(summaryContext, userAndUid, global::Snap.Hutao.Service.AvatarInfo.RefreshOptionKind.None, CancellationToken).ConfigureAwait(false); + + if (summary is not { Avatars: { } avatars }) + { + messenger.Send(InfoBarMessage.Warning(SH.ViewPageAvatarPropertyDefaultDescription)); + return; + } + + if (avatars.Source.Count < 1) + { + messenger.Send(InfoBarMessage.Warning(SH.ViewPageAvatarPropertyDefaultDescription)); + return; + } + + ImmutableArray targetAvatars = [.. avatars.Source]; + + BatchCultivateResult? batchResult = await avatarPropertyBatchCultivateService + .ExecuteAsync(summaryContext, targetAvatars, CancellationToken) + .ConfigureAwait(false); + + if (batchResult is not { } result) + { + return; + } + + messenger.Send(CultivationProjectEntriesChangedMessage.Empty); + + InfoBarMessage message = result.SkippedCount > 0 + ? InfoBarMessage.Warning(SH.FormatViewModelCultivationBatchAddIncompleted(result.SucceedCount, result.SkippedCount)) + : InfoBarMessage.Success(SH.FormatViewModelCultivationBatchAddCompleted(result.SucceedCount, result.SkippedCount)); + + messenger.Send(message); + } + [Command("ClearInventoryCommand")] private async Task ClearInventoryAsync(CultivateProject? project) {