diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs new file mode 100644 index 0000000..a23014a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs @@ -0,0 +1,668 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Model.Entity.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260503131124_AddCultivateEntryRelatedEntryId")] + partial class AddCultivateEntryRelatedEntryId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Info2") + .HasColumnType("TEXT"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("avatar_infos"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarStrategy", b => + { + b.Property("AvatarId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChineseStrategyId") + .HasColumnType("INTEGER"); + + b.Property("OverseaStrategyId") + .HasColumnType("INTEGER"); + + b.HasKey("AvatarId"); + + b.ToTable("avatar_strategies"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntryId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("RelatedEntryId"); + + b.ToTable("cultivate_entries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarIsPromoting") + .HasColumnType("INTEGER"); + + b.Property("AvatarLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("AvatarLevelTo") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("SkillALevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillALevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillELevelTo") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("SkillQLevelTo") + .HasColumnType("INTEGER"); + + b.Property("WeaponIsPromoting") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelFrom") + .HasColumnType("INTEGER"); + + b.Property("WeaponLevelTo") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.ToTable("cultivate_entry_level_informations"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ServerTimeZoneOffset") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("cultivate_projects"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DailyNote") + .HasColumnType("TEXT"); + + b.Property("DailyTaskDotVisible") + .HasColumnType("INTEGER"); + + b.Property("DailyTaskNotify") + .HasColumnType("INTEGER"); + + b.Property("DailyTaskNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionDotVisible") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotify") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinDotVisible") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("ResinDotVisible") + .HasColumnType("INTEGER"); + + b.Property("ResinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ResinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("TransformerDotVisible") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotify") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("UserId"); + + b.ToTable("daily_notes"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("gacha_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MacAddress") + .HasColumnType("TEXT"); + + b.Property("Mid") + .HasColumnType("TEXT"); + + b.Property("MihoyoSDK") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("game_accounts"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.HardChallengeEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("HardChallengeData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("hard_challenges"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ExpireTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("object_cache"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.RoleCombatEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("RoleCombatData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("role_combats"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SpiralAbyss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("spiral_abysses"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.UidProfilePicture", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarId") + .HasColumnType("INTEGER"); + + b.Property("CostumeId") + .HasColumnType("INTEGER"); + + b.Property("ProfilePictureId") + .HasColumnType("INTEGER"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("uid_profile_pictures"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Aid") + .HasColumnType("TEXT"); + + b.Property("CookieToken") + .HasColumnType("TEXT"); + + b.Property("CookieTokenLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("Fingerprint") + .HasColumnType("TEXT"); + + b.Property("FingerprintLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsOversea") + .HasColumnType("INTEGER"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LToken") + .HasColumnType("TEXT") + .HasColumnName("Ltoken"); + + b.Property("Mid") + .HasColumnType("TEXT"); + + b.Property("PreferredUid") + .HasColumnType("TEXT"); + + b.Property("SToken") + .HasColumnType("TEXT") + .HasColumnName("Stoken"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry") + .WithMany() + .HasForeignKey("RelatedEntryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Project"); + + b.Navigation("RelatedEntry"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") + .WithOne("LevelInformation") + .HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.HasOne("Snap.Hutao.Model.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.Navigation("LevelInformation"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.cs new file mode 100644 index 0000000..2e9c2be --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + /// + public partial class AddCultivateEntryRelatedEntryId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RelatedEntryId", + table: "cultivate_entries", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_cultivate_entries_RelatedEntryId", + table: "cultivate_entries", + column: "RelatedEntryId"); + + migrationBuilder.AddForeignKey( + name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId", + table: "cultivate_entries", + column: "RelatedEntryId", + principalTable: "cultivate_entries", + principalColumn: "InnerId", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId", + table: "cultivate_entries"); + + migrationBuilder.DropIndex( + name: "IX_cultivate_entries_RelatedEntryId", + table: "cultivate_entries"); + + migrationBuilder.DropColumn( + name: "RelatedEntryId", + table: "cultivate_entries"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 17d1e22..58a702c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => { @@ -113,6 +113,9 @@ namespace Snap.Hutao.Migrations b.Property("ProjectId") .HasColumnType("TEXT"); + b.Property("RelatedEntryId") + .HasColumnType("TEXT"); + b.Property("Type") .HasColumnType("INTEGER"); @@ -120,6 +123,8 @@ namespace Snap.Hutao.Migrations b.HasIndex("ProjectId"); + b.HasIndex("RelatedEntryId"); + b.ToTable("cultivate_entries"); }); @@ -585,7 +590,14 @@ namespace Snap.Hutao.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry") + .WithMany() + .HasForeignKey("RelatedEntryId") + .OnDelete(DeleteBehavior.SetNull); + b.Navigation("Project"); + + b.Navigation("RelatedEntry"); }); modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs index 3a00702..78c2219 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs @@ -26,6 +26,11 @@ internal sealed class CultivateEntry : IAppDbEntity public uint Id { get; set; } + public Guid? RelatedEntryId { get; set; } + + [ForeignKey(nameof(RelatedEntryId))] + public CultivateEntry? RelatedEntry { get; set; } + public static CultivateEntry From(Guid projectId, CultivateType type, uint id) { return new() 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 cf80d67..ee96adc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx @@ -1880,6 +1880,9 @@ Space Available: {2} Ascending only + + Linked character: {0} + Added successfully diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 5719d09..bffdee1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1931,6 +1931,9 @@ 仅突破 + + 关联角色:{0} + 添加成功 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/Consumption/ConsumptionSaveResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/Consumption/ConsumptionSaveResult.cs new file mode 100644 index 0000000..c24a51d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/Consumption/ConsumptionSaveResult.cs @@ -0,0 +1,8 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Cultivation.Consumption; + +internal readonly record struct ConsumptionSaveResult( + ConsumptionSaveResultKind Kind, + Guid? CreatedEntryInnerId = null); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs index c1b01e9..b7eda10 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Snap.Hutao.Core.Database; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Service.Abstraction; using System.Collections.Immutable; using System.Collections.ObjectModel; @@ -51,6 +52,26 @@ internal sealed partial class CultivationRepository : ICultivationRepository return this.ImmutableArray(e => e.ProjectId == projectId && e.Id == itemId); } + public Guid? TryGetAvatarCultivateEntryInnerId(Guid projectId, uint avatarId) + { + ImmutableArray entries = GetCultivateEntryImmutableArrayByProjectIdAndItemId(projectId, avatarId); + Guid? last = null; + foreach (ref readonly CultivateEntry entry in entries.AsSpan()) + { + if (entry.Type != CultivateType.AvatarAndSkill) + { + continue; + } + + if (last is null || entry.InnerId.CompareTo(last.Value) > 0) + { + last = entry.InnerId; + } + } + + return last; + } + public void AddCultivateEntry(CultivateEntry entry) { this.Add(entry); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 75f0b82..73a19b3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -67,6 +67,12 @@ internal sealed partial class CultivationService : ICultivationService { ImmutableArray entries = cultivationRepository.GetCultivateEntryImmutableArrayIncludingLevelInformationByProjectId(cultivateProject.InnerId); + Dictionary entryByInnerId = new(entries.Length); + foreach (ref readonly CultivateEntry entry in entries.AsSpan()) + { + entryByInnerId[entry.InnerId] = entry; + } + List resultEntries = new(entries.Length); foreach (ref readonly CultivateEntry entry in entries.AsSpan()) { @@ -87,7 +93,13 @@ internal sealed partial class CultivationService : ICultivationService _ => default!, }; - resultEntries.Add(CultivateEntryView.Create(entry, item, entryItems.ToImmutable())); + string? relatedAvatarName = null; + if (entry.Type is CultivateType.Weapon && entry.RelatedEntryId is Guid relatedId && entryByInnerId.TryGetValue(relatedId, out CultivateEntry? relatedEntry) && relatedEntry.Type is CultivateType.AvatarAndSkill) + { + relatedAvatarName = context.GetAvatarItem(relatedEntry.Id).Name; + } + + resultEntries.Add(CultivateEntryView.Create(entry, item, entryItems.ToImmutable(), relatedAvatarName)); } ObservableCollection result = resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection(); @@ -156,13 +168,13 @@ internal sealed partial class CultivationService : ICultivationService cultivationRepository.UpdateCultivateItem(item.Entity); } - public async ValueTask SaveConsumptionAsync(InputConsumption inputConsumption) + public async ValueTask SaveConsumptionAsync(InputConsumption inputConsumption) { // No selected project IAdvancedDbCollectionView projects = await GetProjectCollectionAsync().ConfigureAwait(false); if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false)) { - return ConsumptionSaveResultKind.NoProject; + return new(ConsumptionSaveResultKind.NoProject); } ArgumentNullException.ThrowIfNull(projects.CurrentItem); @@ -172,7 +184,7 @@ internal sealed partial class CultivationService : ICultivationService // PreserveExisting or CreateNewEntry, but no item if (inputConsumption is { Strategy: not ConsumptionSaveStrategyKind.OverwriteExisting, Items: [] }) { - return ConsumptionSaveResultKind.NoItem; + return new(ConsumptionSaveResultKind.NoItem); } // PreserveExisting or OverwriteExisting @@ -185,7 +197,7 @@ internal sealed partial class CultivationService : ICultivationService { if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting) { - return ConsumptionSaveResultKind.Skipped; + return new(ConsumptionSaveResultKind.Skipped); } if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.OverwriteExisting) @@ -199,7 +211,7 @@ internal sealed partial class CultivationService : ICultivationService if (inputConsumption.Items is []) { - return ConsumptionSaveResultKind.Removed; + return new(ConsumptionSaveResultKind.Removed); } } } @@ -207,13 +219,14 @@ internal sealed partial class CultivationService : ICultivationService { if (inputConsumption.Items is []) { - return ConsumptionSaveResultKind.NoItem; + return new(ConsumptionSaveResultKind.NoItem); } } } { CultivateEntry entry = CultivateEntry.From(projects.CurrentItem.InnerId, inputConsumption.Type, inputConsumption.ItemId); + entry.RelatedEntryId = inputConsumption.RelatedEntryId; cultivationRepository.AddCultivateEntry(entry); CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entry.InnerId, inputConsumption.Type, inputConsumption.LevelInformation); @@ -225,9 +238,23 @@ internal sealed partial class CultivationService : ICultivationService // The consumption save operation is always performed outside cultivation page // and without touching the cache. So we have to invalidate the cache manually. entryCollectionCache.TryRemove(projects.CurrentItem.InnerId, out _); + + return new(ConsumptionSaveResultKind.Added, entry.InnerId); + } + } + + public async ValueTask TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId) + { + IAdvancedDbCollectionView projects = await GetProjectCollectionAsync().ConfigureAwait(false); + if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false)) + { + return null; } - return ConsumptionSaveResultKind.Added; + ArgumentNullException.ThrowIfNull(projects.CurrentItem); + + await taskContext.SwitchToBackgroundAsync(); + return cultivationRepository.TryGetAvatarCultivateEntryInnerId(projects.CurrentItem.InnerId, avatarId); } public async ValueTask TryAddProjectAsync(CultivateProject project) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs index a6b7754..7a9454e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs @@ -43,6 +43,12 @@ internal interface ICultivationRepository : IRepository GetCultivateEntryImmutableArrayByProjectIdAndItemId(Guid projectId, uint itemId); + /// + /// 解析当前计划中指定角色的养成条目(类型为 CultivateType.AvatarAndSkill)。 + /// 若存在多条历史记录,取条目主键 InnerId(Guid)较大的一条。 + /// + Guid? TryGetAvatarCultivateEntryInnerId(Guid projectId, uint avatarId); + Guid GetCultivateProjectIdByEntryId(Guid entryId); /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index 423f898..c3d2eea 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -25,7 +25,9 @@ internal interface ICultivationService ValueTask RemoveProjectAsync(CultivateProject project); - ValueTask SaveConsumptionAsync(InputConsumption inputConsumption); + ValueTask SaveConsumptionAsync(InputConsumption inputConsumption); + + ValueTask TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId); void SaveCultivateItem(CultivateItemView item); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs index 65db8d5..dd8994b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs @@ -19,4 +19,9 @@ internal sealed class InputConsumption public required LevelInformation LevelInformation { get; init; } public required ConsumptionSaveStrategyKind Strategy { get; init; } + + /// + /// 武器条目关联的养成角色条目的主键(自引用外键,可为空)。 + /// + public Guid? RelatedEntryId { get; init; } } \ No newline at end of file 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 e69d456..403ff70 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 @@ -198,6 +198,11 @@ Opacity="0.7" Style="{StaticResource CaptionTextBlockStyle}" Text="{Binding Description}"/> + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index 713277b..fb69c22 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -335,9 +335,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I Strategy = options.Strategy, }; - ConsumptionSaveResultKind avatarSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false); + ConsumptionSaveResult avatarSave = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false); - InfoBarMessage? avatarMessage = avatarSaveKind switch + InfoBarMessage? avatarMessage = avatarSave.Kind switch { ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning), ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint), @@ -351,13 +351,19 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I scopeContext.Messenger.Send(avatarMessage); } - if (avatarSaveKind is ConsumptionSaveResultKind.NoProject) + 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 scopeContext.CultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false); + } + InputConsumption weaponInput = new() { Type = CultivateType.Weapon, @@ -365,10 +371,11 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I Items = consumption.WeaponConsume, LevelInformation = levelInformation, Strategy = options.Strategy, + RelatedEntryId = relatedAvatarEntryId, }; - ConsumptionSaveResultKind weaponSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false); - InfoBarMessage? weaponMessage = weaponSaveKind switch + ConsumptionSaveResult weaponSave = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false); + InfoBarMessage? weaponMessage = weaponSave.Kind switch { ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning), ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint), @@ -382,7 +389,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I scopeContext.Messenger.Send(weaponMessage); } - return weaponSaveKind is not ConsumptionSaveResultKind.NoProject; + return weaponSave.Kind is not ConsumptionSaveResultKind.NoProject; } [Command("ExportToTextCommand")] diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs index 162007d..b952d62 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs @@ -14,7 +14,7 @@ namespace Snap.Hutao.ViewModel.Cultivation; internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider { - private CultivateEntryView(CultivateEntry entry, Item item, ImmutableArray items) + private CultivateEntryView(CultivateEntry entry, Item item, ImmutableArray items, string? relatedAvatarName) { Id = entry.Id; EntryId = entry.InnerId; @@ -25,6 +25,8 @@ internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider Items = items; Type = entry.Type; + RelatedAvatarLine = relatedAvatarName is { } name ? SH.FormatViewModelCultivationEntryRelatedAvatar(name) : null; + Description = ParseDescription(entry); IsToday = items.Any(i => i.IsToday); RotationalItemIds = [.. items.Where(i => i.DaysOfWeek is not DaysOfWeek.Any).Select(i => i.Inner.Id)]; @@ -110,12 +112,17 @@ internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider public string Description { get; } + /// + /// 武器条目在养成计算中与角色条目关联时的展示文案(含本地化前缀)。 + /// + public string? RelatedAvatarLine { get; } + internal Guid EntryId { get; } internal CultivateType Type { get; } - public static CultivateEntryView Create(CultivateEntry entry, Item item, ImmutableArray items) + public static CultivateEntryView Create(CultivateEntry entry, Item item, ImmutableArray items, string? relatedAvatarName = null) { - return new(entry, item, items); + return new(entry, item, items, relatedAvatarName); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index 5417f05..e2abb06 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -155,7 +155,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel Strategy = deltaOptions.Strategy, }; - InfoBarMessage? message = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false) switch + InfoBarMessage? message = (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false)).Kind switch { ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning), ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint), diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index d8f0575..1484d59 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -146,7 +146,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel Strategy = deltaOptions.Strategy, }; - InfoBarMessage? message = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false) switch + InfoBarMessage? message = (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false)).Kind switch { ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning), ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),