From ff77d5e5b65d014ed71268927b13df6180d012fb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 27 Jun 2026 16:33:59 -0400 Subject: [PATCH] =?UTF-8?q?fix(guild):=20final-review=20remediation=20?= =?UTF-8?q?=E2=80=94=20replay=5Fdetail=20flat,=20FK=20on=20leader,=20trans?= =?UTF-8?q?action=20wraps,=20=5Fdb=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: replay_detail — flatten stored payload to data level. ChatReplayDetailTask.Parse() calls new ReplayDetailInfo(data) which unguardedly accesses data[battleId], data[seed], data[vid1], etc. Wrapping under replay_info key crashes the client. Controller now returns Ok(JsonElement) directly so stored battle fields are at data root. Wire-shape test added. C2: Guild.LeaderViewerId long to long?; add HasOne FK with OnDelete=SetNull; migration AddGuildLeaderViewerIdFk; all consumers null-guarded with ?? 0L. C3: BreakupAsync — wrap 6 destructive ops in IDbContextTransaction with InMemory fallback. C4: CommitJoinAsync — wrap member-add + viewer-guildId-set + invite/request cleanup in IDbContextTransaction with InMemory fallback. Chat event emitted after commit. I1: GuildController — remove SVSimDbContext field; inject IViewerRepository + IGuildMemberRepository. Add IGuildMemberRepository.GetViewerIdsInAGuildAsync (batch guild-membership check). All _db.Viewers / _db.GuildMembers queries replaced with repo calls. I2: GuildService — extract 3 _db.Viewers queries: GetEquippedEmblemIdAsync (CreateAsync), LoadGuildProfileBatchAsync (ListOutgoingInvitesAsync + ListPendingJoinRequestsForMyGuildAsync). Add GuildMemberProfile record with IsOfficialMarkDisplayed. GetEmblemListAsync for EmblemList. Co-Authored-By: Claude Sonnet 4.6 --- SVSim.Database/Entities/Guild/Guild.cs | 2 +- ...02442_AddGuildLeaderViewerIdFk.Designer.cs | 5018 +++++++++++++++++ ...20260627202442_AddGuildLeaderViewerIdFk.cs | 57 + .../Migrations/SVSimDbContextModelSnapshot.cs | 12 +- .../Guild/GuildMemberRepository.cs | 11 + .../Guild/IGuildMemberRepository.cs | 7 + .../Repositories/Viewer/IViewerRepository.cs | 31 + .../Repositories/Viewer/ViewerRepository.cs | 50 + SVSim.Database/SVSimDbContext.cs | 3 + SVSim.Database/Services/Guild/GuildService.cs | 140 +- .../Controllers/GuildChatController.cs | 10 +- .../Controllers/GuildController.cs | 105 +- .../GuildChatReplayDetailResponse.cs | 34 +- SVSim.UnitTests/Wire/GuildWireShape.cs | 34 + 14 files changed, 5378 insertions(+), 136 deletions(-) create mode 100644 SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.Designer.cs create mode 100644 SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.cs diff --git a/SVSim.Database/Entities/Guild/Guild.cs b/SVSim.Database/Entities/Guild/Guild.cs index 3cb50085..03860d54 100644 --- a/SVSim.Database/Entities/Guild/Guild.cs +++ b/SVSim.Database/Entities/Guild/Guild.cs @@ -7,7 +7,7 @@ public class Guild public int GuildId { get; set; } // server-generated 9-digit public string Name { get; set; } = ""; public string Description { get; set; } = ""; - public long LeaderViewerId { get; set; } + public long? LeaderViewerId { get; set; } public long EmblemId { get; set; } public GuildActivity Activity { get; set; } public GuildJoinCondition JoinCondition { get; set; } diff --git a/SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.Designer.cs b/SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.Designer.cs new file mode 100644 index 00000000..cf5f9f42 --- /dev/null +++ b/SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.Designer.cs @@ -0,0 +1,5018 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SVSim.Database; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + [DbContext(typeof(SVSimDbContext))] + [Migration("20260627202442_AddGuildLeaderViewerIdFk")] + partial class AddGuildLeaderViewerIdFk + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("ShortUdidSequence") + .StartsAt(400000000L); + + modelBuilder.Entity("DegreeEntryViewer", b => + { + b.Property("DegreesId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("DegreesId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("DegreeEntryViewer"); + }); + + modelBuilder.Entity("EmblemEntryViewer", b => + { + b.Property("EmblemsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("EmblemsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("EmblemEntryViewer"); + }); + + modelBuilder.Entity("LeaderSkinEntryViewer", b => + { + b.Property("LeaderSkinsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("LeaderSkinsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("LeaderSkinEntryViewer"); + }); + + modelBuilder.Entity("MyPageBackgroundEntryViewer", b => + { + b.Property("MyPageBackgroundsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("MyPageBackgroundsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("MyPageBackgroundEntryViewer"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.Guild", b => + { + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("Activity") + .HasColumnType("integer"); + + b.Property("BreakupAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EmblemId") + .HasColumnType("bigint"); + + b.Property("JoinCondition") + .HasColumnType("integer"); + + b.Property("LeaderViewerId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("GuildId"); + + b.HasIndex("LeaderViewerId"); + + b.HasIndex("Name"); + + b.ToTable("Guilds"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorViewerId") + .HasColumnType("bigint"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckPayload") + .HasColumnType("jsonb"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("MessageId") + .HasColumnType("integer"); + + b.Property("MessageType") + .HasColumnType("integer"); + + b.Property("ReplayPayload") + .HasColumnType("jsonb"); + + b.Property("RoomPayload") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "MessageId") + .IsUnique(); + + b.ToTable("GuildChatMessages"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("InviteeViewerId") + .HasColumnType("bigint"); + + b.Property("InviterViewerId") + .HasColumnType("bigint"); + + b.Property("RespondedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "InviteeViewerId") + .IsUnique() + .HasFilter("\"Status\" = 0"); + + b.HasIndex("InviteeViewerId", "Status"); + + b.ToTable("GuildInvites"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildJoinRequest", b => + { + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RespondedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("GuildId", "ViewerId"); + + b.HasIndex("GuildId", "Status"); + + b.HasIndex("ViewerId", "Status"); + + b.ToTable("GuildJoinRequests"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildMember", b => + { + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("GuildId", "ViewerId"); + + b.HasIndex("ViewerId") + .IsUnique(); + + b.ToTable("GuildMembers"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.SpecialBattleSetting", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BanishEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClassDestroyEffectOverride") + .HasColumnType("integer"); + + b.Property("EnemyAttachSkill") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnemyStartLife") + .HasColumnType("integer"); + + b.Property("EnemyStartPp") + .HasColumnType("integer"); + + b.Property("IdOverrideInBattleLog") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PlayerAttachSkill") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerFirstTurn") + .HasColumnType("integer"); + + b.Property("PlayerStartLife") + .HasColumnType("integer"); + + b.Property("PlayerStartPp") + .HasColumnType("integer"); + + b.Property("ResultSkip") + .HasColumnType("integer"); + + b.Property("SpecialTokenDrawEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenDrawEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("VsEffectOverride") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SpecialBattleSettings"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b => + { + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("Battle3dFieldId") + .HasColumnType("integer"); + + b.Property("BattleExists") + .HasColumnType("boolean"); + + b.Property("BgFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BgmId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterClearTextId") + .HasColumnType("text"); + + b.Property("ChapterEffectPath") + .HasColumnType("text"); + + b.Property("ChapterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("EnemyAiId") + .HasColumnType("integer"); + + b.Property("EnemyCharaId") + .HasColumnType("integer"); + + b.Property("EnemyClass") + .HasColumnType("integer"); + + b.Property("IsCameraMovable") + .HasColumnType("integer"); + + b.Property("IsMaintenanceChapter") + .HasColumnType("boolean"); + + b.Property("IsPlayAnotherEndAppearanceAnimation") + .HasColumnType("boolean"); + + b.Property("IsReleasedAnotherEnd") + .HasColumnType("boolean"); + + b.Property("IsSkipEnabled") + .HasColumnType("boolean"); + + b.Property("NextChapterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReleasePoint") + .HasColumnType("integer"); + + b.Property("RequiredChapterId") + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("integer"); + + b.Property("SelectionDisplayPosition") + .HasColumnType("text"); + + b.Property("SelectionTextId") + .HasColumnType("text"); + + b.Property("ShowCoordinate") + .HasColumnType("integer"); + + b.Property("ShowSubtitles") + .HasColumnType("integer"); + + b.Property("SpecialBattleSettingId") + .HasColumnType("integer"); + + b.Property("UnlockText") + .HasColumnType("text"); + + b.Property("XCoordinate") + .HasColumnType("numeric"); + + b.Property("YCoordinate") + .HasColumnType("numeric"); + + b.HasKey("StoryId"); + + b.HasIndex("NextChapterId"); + + b.HasIndex("SpecialBattleSettingId"); + + b.HasIndex("SectionId", "CharaId", "ChapterId"); + + b.ToTable("StoryChapters"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AllStoryOrderId") + .HasColumnType("integer"); + + b.Property("BackGroundId") + .HasColumnType("integer"); + + b.Property("ChapterSelectType") + .HasColumnType("integer"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLeaderSelect") + .HasColumnType("boolean"); + + b.Property("IsPlayAnotherEndAppearanceAnimation") + .HasColumnType("boolean"); + + b.Property("IsSpoiler") + .HasColumnType("integer"); + + b.Property("IsUnderMaintenance") + .HasColumnType("boolean"); + + b.Property("NameTextKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("SpoilerMessage") + .IsRequired() + .HasColumnType("text"); + + b.Property("StoryApiType") + .HasColumnType("integer"); + + b.Property("StoryTypeOverwrite") + .HasColumnType("integer"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("StorySections"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryWorld", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("PanelImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RibbonText") + .IsRequired() + .HasColumnType("text"); + + b.Property("TitleTextKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("StoryWorlds"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryBranchUnlock", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("UnlockedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "StoryId"); + + b.ToTable("ViewerStoryBranchUnlocks"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryProgress", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsFinish") + .HasColumnType("boolean"); + + b.Property("IsSkipped") + .HasColumnType("boolean"); + + b.Property("SkippedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "StoryId"); + + b.ToTable("ViewerStoryProgress"); + }); + + modelBuilder.Entity("SVSim.Database.Models.AchievementCatalogEntry", b => + { + b.Property("AchievementType") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("AchievementType", "Level"); + + b.HasIndex("AchievementType"); + + b.HasIndex("EventType", "EventArg"); + + b.ToTable("AchievementCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Cost") + .HasColumnType("numeric(20,0)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Enable") + .HasColumnType("integer"); + + b.Property("FormatInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsJoin") + .HasColumnType("boolean"); + + b.Property("Mode") + .HasColumnType("integer"); + + b.Property("RupyCost") + .HasColumnType("numeric(20,0)"); + + b.Property("TicketCost") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ArenaSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ArenaTwoPickReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("RewardGroup") + .HasColumnType("integer"); + + b.Property("RewardId") + .HasColumnType("bigint"); + + b.Property("RewardNum") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.Property("WinCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WinCount"); + + b.HasIndex("WinCount", "RewardGroup", "RewardType", "RewardId", "RewardNum") + .IsUnique(); + + b.ToTable("ArenaTwoPickRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.AvatarAbilityEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Ability") + .IsRequired() + .HasColumnType("text"); + + b.Property("AbilityCost") + .IsRequired() + .HasColumnType("text"); + + b.Property("AbilityDesc") + .IsRequired() + .HasColumnType("text"); + + b.Property("BattleStartFirstPlayerTurnBp") + .HasColumnType("integer"); + + b.Property("BattleStartMaxLife") + .HasColumnType("integer"); + + b.Property("BattleStartSecondPlayerTurnBp") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("PassiveAbility") + .IsRequired() + .HasColumnType("text"); + + b.Property("PassiveAbilityDesc") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AvatarAbilities"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BannerEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ChangeTime") + .HasColumnType("integer"); + + b.Property("Click") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImagePaths") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Banners"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassLevelEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("RequiredPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("BattlePassLevels"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassMonthlyMissionEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattlePassPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("Month") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Month"); + + b.HasIndex("Year", "Month", "OrderNum") + .IsUnique(); + + b.ToTable("BattlePassMonthlyMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAppealExclusion") + .HasColumnType("boolean"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Track") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Track", "Level") + .IsUnique(); + + b.ToTable("BattlePassRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassSeasonEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CanPurchase") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceCrystal") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("StartDate", "EndDate"); + + b.ToTable("BattlePassSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlefieldEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsOpen") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Battlefields"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BotRosterEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AiId") + .HasColumnType("integer"); + + b.Property("BattlePoint") + .HasColumnType("integer"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("CountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DegreeId") + .HasColumnType("integer"); + + b.Property("EmblemId") + .HasColumnType("integer"); + + b.Property("FieldId") + .HasColumnType("integer"); + + b.Property("IsMasterRank") + .HasColumnType("integer"); + + b.Property("IsOfficial") + .HasColumnType("integer"); + + b.Property("MasterPoint") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BotRoster"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("FeaturedCardId") + .HasColumnType("bigint"); + + b.Property("IntroPriceCrystal") + .HasColumnType("integer"); + + b.Property("IntroPriceRupy") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LeaderId") + .HasColumnType("integer"); + + b.Property("ProductNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PurchaseNumMax") + .HasColumnType("integer"); + + b.Property("RegularPriceCrystal") + .HasColumnType("integer"); + + b.Property("RegularPriceRupy") + .HasColumnType("integer"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("BuildDeckProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DrumrollPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("IntroKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("NameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderIndex") + .HasColumnType("integer"); + + b.Property("TitlePath") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BuildDeckSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b => + { + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("CosmeticId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("CardId", "Type", "CosmeticId"); + + b.HasIndex("CardId"); + + b.ToTable("CardCosmeticRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Classes"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassExpEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("NecessaryExp") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClassExpCurve"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ColosseumAvatarDeck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("bigint"); + + b.Property("SleeveId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DeckNo") + .IsUnique(); + + b.ToTable("ColosseumAvatarDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ColosseumHofDeck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("bigint"); + + b.Property("SleeveId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DeckNo") + .IsUnique(); + + b.ToTable("ColosseumHofDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ColosseumWindFallDeck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("bigint"); + + b.Property("SleeveId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DeckNo") + .IsUnique(); + + b.ToTable("ColosseumWindFallDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DefaultDeckEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardIdArray") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("DefaultDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DegreeEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Degrees"); + }); + + modelBuilder.Entity("SVSim.Database.Models.EmblemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Emblems"); + }); + + modelBuilder.Entity("SVSim.Database.Models.FeatureMaintenanceEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("FeatureMaintenances"); + }); + + modelBuilder.Entity("SVSim.Database.Models.GameConfigSection", b => + { + b.Property("SectionName") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ValueJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("SectionName"); + + b.ToTable("GameConfigs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.HomeDialogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BeginTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ButtonListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Image") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("TitleTextId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HomeDialogEntries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ItemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ThumbnailPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ItemPurchaseCatalogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsMonthlyReset") + .HasColumnType("boolean"); + + b.Property("PurchaseItemId") + .HasColumnType("bigint"); + + b.Property("PurchaseItemNum") + .HasColumnType("integer"); + + b.Property("PurchaseItemType") + .HasColumnType("integer"); + + b.Property("PurchaseLimit") + .HasColumnType("integer"); + + b.Property("PurchaseName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequireItemId") + .HasColumnType("bigint"); + + b.Property("RequireItemNum") + .HasColumnType("integer"); + + b.Property("RequireItemType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemPurchaseCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EmoteId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.ToTable("LeaderSkins"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CvNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IntroductionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("ProductNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.Property("SinglePriceCrystal") + .HasColumnType("integer"); + + b.Property("SinglePriceRupy") + .HasColumnType("integer"); + + b.Property("SinglePriceTicket") + .HasColumnType("integer"); + + b.Property("TicketItemId") + .HasColumnType("bigint"); + + b.Property("TicketNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("LeaderSkinShopProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("SetCompletionRewardStatus") + .HasColumnType("integer"); + + b.Property("SetPriceCrystal") + .HasColumnType("integer"); + + b.Property("SetPriceRupy") + .HasColumnType("integer"); + + b.Property("SetPriceTicket") + .HasColumnType("integer"); + + b.Property("SetPriceTicketId") + .HasColumnType("bigint"); + + b.Property("SetSalesStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("LeaderSkinShopSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LoadingExclusionCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LoadingExclusionCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MaintenanceCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MaintenanceCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MasterPointRankingPeriodEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BeginTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("NecessaryScore") + .HasColumnType("bigint"); + + b.Property("PeriodNum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MasterPointRankingPeriods"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MissionCatalogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BattlePassPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultFlag") + .HasColumnType("boolean"); + + b.Property("EndTime") + .HasColumnType("bigint"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("LotType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("LotType"); + + b.HasIndex("EventType", "EventArg"); + + b.ToTable("MissionCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyPageBackgroundEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MyPageBackgrounds"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyRotationAbilityEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AbilityId") + .HasColumnType("integer"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MyRotationAbilities"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyRotationSettingEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AbilitiesCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardSetIdsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ReprintedCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RestrictedCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RotationId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MyRotationSettings"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BasePackId") + .HasColumnType("integer"); + + b.Property("CommenceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CompleteDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("GachaDetail") + .IsRequired() + .HasColumnType("text"); + + b.Property("GachaType") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsHide") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("OpenCountLimit") + .HasColumnType("integer"); + + b.Property("OverrideDrawEffectPackId") + .HasColumnType("integer"); + + b.Property("OverrideUiEffectPackId") + .HasColumnType("integer"); + + b.Property("PackCategory") + .HasColumnType("integer"); + + b.Property("PosterType") + .HasColumnType("integer"); + + b.Property("SalesPeriodTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("SpecialSleeveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Packs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackDrawCardWeightEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAltArt") + .HasColumnType("boolean"); + + b.Property("IsLeader") + .HasColumnType("boolean"); + + b.Property("PackId") + .HasColumnType("integer"); + + b.Property("RatePct") + .HasColumnType("double precision"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PackId", "Slot", "Tier"); + + b.ToTable("PackDrawCardWeights"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackDrawConfigEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AnimationRatePct") + .HasColumnType("double precision"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("HasBonusSlot") + .HasColumnType("boolean"); + + b.Property("ShortCode") + .HasColumnType("text"); + + b.Property("SpecialKind") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PackDrawConfigs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackDrawSlotRateEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("PackId") + .HasColumnType("integer"); + + b.Property("RatePct") + .HasColumnType("double precision"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PackId", "Slot", "Tier") + .IsUnique(); + + b.ToTable("PackDrawSlotRates"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PaymentItemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ChargeCrystalNum") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FreeCrystalNum") + .HasColumnType("integer"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsResaleProduct") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("PurchaseLimit") + .HasColumnType("integer"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("ResaleStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SpecialShopFlag") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StoreProductId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PaymentItems"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PracticeOpponentEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AiDeckLevel") + .HasColumnType("integer"); + + b.Property("AiLogicLevel") + .HasColumnType("integer"); + + b.Property("AiMaxLife") + .HasColumnType("integer"); + + b.Property("Battle3dFieldId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DegreeId") + .HasColumnType("integer"); + + b.Property("IsCampaignPractice") + .HasColumnType("boolean"); + + b.Property("IsMaintenance") + .HasColumnType("boolean"); + + b.Property("PracticeId") + .HasColumnType("integer"); + + b.Property("TextId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PracticeOpponents"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PreReleaseInfo", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardMasterId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCardMasterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisplayEndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FreeMatchStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRotationFreeMatchTerm") + .HasColumnType("boolean"); + + b.Property("LatestReprintedBaseCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("NextCardSetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreReleaseCardMasterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreReleaseId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReprintedBaseCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RotationCardSetIdList") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("PreReleaseInfos"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("IsAdditional") + .HasColumnType("boolean"); + + b.Property("IsPlayable") + .HasColumnType("boolean"); + + b.Property("PuzzleDifficulty") + .HasColumnType("integer"); + + b.Property("PuzzleId") + .HasColumnType("integer"); + + b.Property("ReleaseConditionTextId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("Puzzles"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BasicTitleTextId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DifficultyNameListJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PuzzleCharaId") + .HasColumnType("integer"); + + b.Property("PuzzleMasterId") + .HasColumnType("integer"); + + b.Property("SortType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PuzzleGroups"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleMissionEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AchievedMessage") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignCommenceTime") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("TargetPuzzleGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PuzzleMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.RankInfoEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AccumulateMasterPoint") + .HasColumnType("integer"); + + b.Property("AccumulatePoint") + .HasColumnType("integer"); + + b.Property("BaseAddBp") + .HasColumnType("integer"); + + b.Property("BaseDropBp") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPromotionWar") + .HasColumnType("integer"); + + b.Property("LoseBonus") + .HasColumnType("double precision"); + + b.Property("LowerLimitPoint") + .HasColumnType("integer"); + + b.Property("MatchCount") + .HasColumnType("integer"); + + b.Property("MaxLoseBonus") + .HasColumnType("integer"); + + b.Property("MaxWinBonus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NecessaryPoint") + .HasColumnType("integer"); + + b.Property("NecessaryWin") + .HasColumnType("integer"); + + b.Property("ResetLose") + .HasColumnType("integer"); + + b.Property("StreakBonusPt") + .HasColumnType("integer"); + + b.Property("WinBonus") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("RankInfo"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ReprintedCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ReprintedCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SealedConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CrystalCost") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckUsingNumMin") + .HasColumnType("integer"); + + b.Property("Enable") + .HasColumnType("integer"); + + b.Property("IsDeckCodeMaintenance") + .HasColumnType("boolean"); + + b.Property("IsJoin") + .HasColumnType("boolean"); + + b.Property("PackInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RupyCost") + .HasColumnType("integer"); + + b.Property("SalesPeriodInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScheduleId") + .HasColumnType("integer"); + + b.Property("TicketCost") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SealedSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SerialCodeEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SerialCodes"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SerialCodeRewardEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("RewardCount") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SerialCodeId") + .HasColumnType("integer"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SerialCodeId", "Slot"); + + b.ToTable("SerialCodeRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Attack") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Defense") + .HasColumnType("integer"); + + b.Property("IsFoil") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrimaryResourceCost") + .HasColumnType("integer"); + + b.Property("Rarity") + .HasColumnType("integer"); + + b.Property("ShadowverseCardSetEntryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("ShadowverseCardSetEntryId"); + + b.ToTable("Cards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBasic") + .HasColumnType("boolean"); + + b.Property("IsInRotation") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CardSets"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Format") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("MyRotationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("RandomLeaderSkin") + .HasColumnType("boolean"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("LeaderSkinId"); + + b.HasIndex("SleeveId"); + + b.HasIndex("ViewerId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Sleeves"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("NameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceCrystal") + .HasColumnType("integer"); + + b.Property("PriceRupy") + .HasColumnType("integer"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SleeveShopProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("SleeveShopSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpecialDeckFormatEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("SpecialDeckFormats"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpotCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("Cost") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("SpotCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpotCardExchangeEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ExchangePoint") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("TsRotationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SpotCardExchangeCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.StoryDeckEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .HasColumnType("integer"); + + b.Property("DeckName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("EntryNo") + .HasColumnType("integer"); + + b.Property("IsRecommend") + .HasColumnType("integer"); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("StoryDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.TutorialPresentEntry", b => + { + b.Property("PresentId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("RewardCount") + .HasColumnType("bigint"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("PresentId"); + + b.ToTable("TutorialPresentEntries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("RestrictionValue") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UnlimitedRestrictions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLoginBonusClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoginBonusStreak") + .HasColumnType("integer"); + + b.Property("MyPageBgId") + .HasColumnType("integer"); + + b.Property("MyPageBgSelectType") + .HasColumnType("integer"); + + b.Property("ShortUdid") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValueSql("nextval('\"ShortUdidSequence\"')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("ShortUdid"), "ShortUdidSequence"); + + b.Property("Udid") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("ShortUdid"); + + b.HasIndex("Udid") + .IsUnique(); + + b.ToTable("Viewers"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("AchievementType") + .HasColumnType("integer"); + + b.Property("AchievementStatus") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("NowAchievedLevel") + .HasColumnType("integer"); + + b.Property("ResultAnnounceSawLevel") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "AchievementType"); + + b.ToTable("ViewerAchievements"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAcquireHistoryEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcquireTime") + .HasColumnType("timestamp with time zone"); + + b.Property("AcquireType") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RewardCount") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "AcquireTime", "Id"); + + b.ToTable("ViewerAcquireHistory"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerArenaColosseumRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleCountThisRound") + .HasColumnType("integer"); + + b.Property("BreakthroughNumberThisRound") + .HasColumnType("integer"); + + b.Property("CandidateClassIdsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ChaosId") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("ConsumeItemType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .HasColumnType("integer"); + + b.Property("EntryId") + .HasColumnType("bigint"); + + b.Property("IsChampion") + .HasColumnType("boolean"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("IsRankMatching") + .HasColumnType("boolean"); + + b.Property("IsSelectCompleted") + .HasColumnType("boolean"); + + b.Property("LeaderSkinId") + .HasColumnType("bigint"); + + b.Property("LossCount") + .HasColumnType("integer"); + + b.Property("MaxBattleCountThisRound") + .HasColumnType("integer"); + + b.Property("NextCandidateId") + .HasColumnType("bigint"); + + b.Property("PendingPickSetsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RegisteredDeckNoListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RestEntryNum") + .HasColumnType("integer"); + + b.Property("ResultListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoundId") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SelectTurn") + .HasColumnType("integer"); + + b.Property("SelectedCardIdsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("WinCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId") + .IsUnique(); + + b.ToTable("ViewerArenaColosseumRuns"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerArenaTwoPickRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateClassIdsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ChallengeId") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntryId") + .HasColumnType("bigint"); + + b.Property("IsRetire") + .HasColumnType("boolean"); + + b.Property("IsSelectCompleted") + .HasColumnType("boolean"); + + b.Property("LeaderSkinId") + .HasColumnType("bigint"); + + b.Property("LossCount") + .HasColumnType("integer"); + + b.Property("MaxBattleCount") + .HasColumnType("integer"); + + b.Property("NextCandidateId") + .HasColumnType("bigint"); + + b.Property("PendingPickSetsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ResultListJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RewardScheduleId") + .HasColumnType("integer"); + + b.Property("SelectTurn") + .HasColumnType("integer"); + + b.Property("SelectedCardIdsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("WinCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId") + .IsUnique(); + + b.ToTable("ViewerArenaTwoPickRuns"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattleHistory", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("BattleId") + .HasColumnType("bigint"); + + b.Property("BattleStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("BattleType") + .HasColumnType("integer"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .HasColumnType("integer"); + + b.Property("IsLimitTurn") + .HasColumnType("integer"); + + b.Property("IsWin") + .HasColumnType("boolean"); + + b.Property("OpponentCharaId") + .HasColumnType("integer"); + + b.Property("OpponentClassId") + .HasColumnType("integer"); + + b.Property("OpponentCountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpponentDegreeId") + .HasColumnType("bigint"); + + b.Property("OpponentEmblemId") + .HasColumnType("bigint"); + + b.Property("OpponentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpponentRotationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpponentSubClassId") + .HasColumnType("integer"); + + b.Property("SelfCharaId") + .HasColumnType("integer"); + + b.Property("SelfClassId") + .HasColumnType("integer"); + + b.Property("SelfRotationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SelfSubClassId") + .HasColumnType("integer"); + + b.Property("TwoPickType") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "BattleId"); + + b.HasIndex("ViewerId", "CreateTime") + .HasDatabaseName("IX_ViewerBattleHistories_ViewerId_CreateTime"); + + b.ToTable("ViewerBattleHistories"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassClaimEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Track") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "SeasonId"); + + b.HasIndex("ViewerId", "SeasonId", "Track", "Level") + .IsUnique(); + + b.ToTable("ViewerBattlePassClaims"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassProgressEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPremium") + .HasColumnType("boolean"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("WeeklyPeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("WeeklyPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "SeasonId") + .IsUnique(); + + b.ToTable("ViewerBattlePassProgress"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("EventKey") + .HasColumnType("text"); + + b.Property("Period") + .HasColumnType("text"); + + b.Property("Count") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "EventKey", "Period"); + + b.HasIndex("ViewerId", "Period"); + + b.ToTable("ViewerEventCounters"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerFriend", b => + { + b.Property("OwnerViewerId") + .HasColumnType("bigint"); + + b.Property("FriendViewerId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("OwnerViewerId", "FriendViewerId"); + + b.HasIndex("FriendViewerId"); + + b.HasIndex("OwnerViewerId", "CreatedAt"); + + b.ToTable("ViewerFriends"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerFriendApply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromViewerId") + .HasColumnType("bigint"); + + b.Property("MissionType") + .HasColumnType("integer"); + + b.Property("ToViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ToViewerId"); + + b.HasIndex("FromViewerId", "ToViewerId") + .IsUnique(); + + b.ToTable("ViewerFriendApplies"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerLeaderSkinSetClaim", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "SeriesId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerLeaderSkinSetClaims"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAt") + .HasColumnType("bigint"); + + b.Property("ClaimedAt") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MissionCatalogId") + .HasColumnType("integer"); + + b.Property("MissionStatus") + .HasColumnType("integer"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId"); + + b.HasIndex("ViewerId", "Slot") + .IsUnique(); + + b.ToTable("ViewerMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPlayedTogether", b => + { + b.Property("OwnerViewerId") + .HasColumnType("bigint"); + + b.Property("OpponentViewerId") + .HasColumnType("bigint"); + + b.Property("BattleType") + .HasColumnType("integer"); + + b.Property("DeckFormat") + .HasColumnType("integer"); + + b.Property("PlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayedMode") + .HasColumnType("integer"); + + b.Property("TwoPickType") + .HasColumnType("integer"); + + b.HasKey("OwnerViewerId", "OpponentViewerId"); + + b.HasIndex("OwnerViewerId", "PlayedAt"); + + b.ToTable("ViewerPlayedTogethers"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPresent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConditionNumber") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("PresentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PresentLimitType") + .HasColumnType("integer"); + + b.Property("RewardCount") + .HasColumnType("bigint"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardLimitTime") + .HasColumnType("bigint"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("Source") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "PresentId") + .IsUnique(); + + b.HasIndex("ViewerId", "Status", "CreatedAt"); + + b.ToTable("ViewerPresents"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPuzzleClear", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("PuzzleId") + .HasColumnType("integer"); + + b.Property("BestRetryCount") + .HasColumnType("integer"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "PuzzleId"); + + b.ToTable("ViewerPuzzleClears"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerSerialCodeRedemption", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("SerialCodeId") + .HasColumnType("integer"); + + b.Property("RedeemedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "SerialCodeId"); + + b.HasIndex("SerialCodeId"); + + b.ToTable("ViewerSerialCodeRedemptions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerSpotCardExchange", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ExchangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.HasKey("ViewerId", "CardId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerSpotCardExchanges"); + }); + + modelBuilder.Entity("SleeveEntryViewer", b => + { + b.Property("SleevesId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("SleevesId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("SleeveEntryViewer"); + }); + + modelBuilder.Entity("DegreeEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.DegreeEntry", null) + .WithMany() + .HasForeignKey("DegreesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EmblemEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.EmblemEntry", null) + .WithMany() + .HasForeignKey("EmblemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LeaderSkinEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.LeaderSkinEntry", null) + .WithMany() + .HasForeignKey("LeaderSkinsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MyPageBackgroundEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.MyPageBackgroundEntry", null) + .WithMany() + .HasForeignKey("MyPageBackgroundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.Guild", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("LeaderViewerId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildChatMessage", b => + { + b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildInvite", b => + { + b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildJoinRequest", b => + { + b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildMember", b => + { + b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild") + .WithMany("Members") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b => + { + b.HasOne("SVSim.Database.Entities.Story.StorySection", "Section") + .WithMany() + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Entities.Story.SpecialBattleSetting", "SpecialBattleSetting") + .WithMany() + .HasForeignKey("SpecialBattleSettingId"); + + b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterBattleSetting", "BattleSettings", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Battle3dFieldIdOverride") + .HasColumnType("integer"); + + b1.Property("BgmIdOverride") + .HasColumnType("integer"); + + b1.Property("DeckClassId") + .HasColumnType("integer"); + + b1.Property("DeckSkinIdOverride") + .HasColumnType("integer"); + + b1.Property("EnemyEmotionOverride") + .HasColumnType("integer"); + + b1.Property("PlayerEmotionOverride") + .HasColumnType("integer"); + + b1.Property("SkinIdOverride") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StoryChapterBattleSetting"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterReward", "Rewards", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StoryChapterReward"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.OwnsMany("SVSim.Database.Entities.Story.StorySubChapter", "SubChapters", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("IsMaintenanceChapter") + .HasColumnType("boolean"); + + b1.Property("SubChapterId") + .HasColumnType("integer"); + + b1.Property("SubChapterStoryId") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StorySubChapter"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.Navigation("BattleSettings"); + + b.Navigation("Rewards"); + + b.Navigation("Section"); + + b.Navigation("SpecialBattleSetting"); + + b.Navigation("SubChapters"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b => + { + b.HasOne("SVSim.Database.Entities.Story.StoryWorld", "World") + .WithMany() + .HasForeignKey("WorldId"); + + b.Navigation("World"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b => + { + b.HasOne("SVSim.Database.Models.BattlePassSeasonEntry", "Season") + .WithMany("Rewards") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckProductEntry", b => + { + b.HasOne("SVSim.Database.Models.BuildDeckSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.BuildDeckProductCardEntry", "Cards", b1 => + { + b1.Property("BuildDeckProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("IsSpot") + .HasColumnType("boolean"); + + b1.Property("Number") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckProductEntryId", "Id"); + + b1.ToTable("BuildDeckProductCardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckProductEntryId"); + }); + + b.OwnsMany("SVSim.Database.Models.BuildDeckProductRewardEntry", "Rewards", b1 => + { + b1.Property("BuildDeckProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("MessageId") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardIndex") + .HasColumnType("integer"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckProductEntryId", "Id"); + + b1.ToTable("BuildDeckProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckProductEntryId"); + }); + + b.Navigation("Cards"); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.OwnsMany("SVSim.Database.Models.BuildDeckSeriesRewardEntry", "SeriesRewards", b1 => + { + b1.Property("BuildDeckSeriesEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ItemIndex") + .HasColumnType("integer"); + + b1.Property("MessageId") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.Property("TierIndex") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckSeriesEntryId", "Id"); + + b1.ToTable("BuildDeckSeriesRewardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckSeriesEntryId"); + }); + + b.Navigation("SeriesRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b => + { + b.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Card"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany("LeaderSkins") + .HasForeignKey("ClassId"); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b => + { + b.HasOne("SVSim.Database.Models.LeaderSkinShopSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.LeaderSkinShopProductRewardEntry", "Rewards", b1 => + { + b1.Property("LeaderSkinShopProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("LeaderSkinShopProductEntryId", "Id"); + + b1.ToTable("LeaderSkinShopProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("LeaderSkinShopProductEntryId"); + }); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.OwnsMany("SVSim.Database.Models.LeaderSkinShopSeriesRewardEntry", "SetCompletionRewards", b1 => + { + b1.Property("LeaderSkinShopSeriesEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("LeaderSkinShopSeriesEntryId", "Id"); + + b1.ToTable("LeaderSkinShopSeriesRewardEntry"); + + b1.WithOwner() + .HasForeignKey("LeaderSkinShopSeriesEntryId"); + }); + + b.Navigation("SetCompletionRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b => + { + b.OwnsMany("SVSim.Database.Models.PackBannerEntry", "Banners", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("BannerName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("DialogTitle") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("PackConfigEntryId", "Id"); + + b1.ToTable("PackBannerEntry"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.OwnsMany("SVSim.Database.Models.PackChildGachaEntry", "ChildGachas", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CampaignName") + .HasColumnType("text"); + + b1.Property("CardCount") + .HasColumnType("integer"); + + b1.Property("Cost") + .HasColumnType("integer"); + + b1.Property("DailyFreeGachaCount") + .HasColumnType("integer"); + + b1.Property("FreeGachaCampaignId") + .HasColumnType("integer"); + + b1.Property("GachaId") + .HasColumnType("integer"); + + b1.Property("IsDailySingle") + .HasColumnType("boolean"); + + b1.Property("ItemId") + .HasColumnType("bigint"); + + b1.Property("OverrideIncreaseGachaPoint") + .HasColumnType("integer"); + + b1.Property("PurchaseLimitCount") + .HasColumnType("integer"); + + b1.Property("TypeDetail") + .HasColumnType("integer"); + + b1.HasKey("PackConfigEntryId", "Id"); + + b1.ToTable("PackChildGachaEntry"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.OwnsOne("SVSim.Database.Models.PackGachaPointConfig", "GachaPointConfig", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("ExchangeablePoint") + .HasColumnType("integer"); + + b1.Property("IncreaseGachaPoint") + .HasColumnType("integer"); + + b1.HasKey("PackConfigEntryId"); + + b1.ToTable("Packs"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.Navigation("Banners"); + + b.Navigation("ChildGachas"); + + b.Navigation("GachaPointConfig"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleEntry", b => + { + b.HasOne("SVSim.Database.Models.PuzzleGroupEntry", "Group") + .WithMany("Puzzles") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SerialCodeRewardEntry", b => + { + b.HasOne("SVSim.Database.Models.SerialCodeEntry", null) + .WithMany("Rewards") + .HasForeignKey("SerialCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId"); + + b.HasOne("SVSim.Database.Models.ShadowverseCardSetEntry", null) + .WithMany("Cards") + .HasForeignKey("ShadowverseCardSetEntryId"); + + b.OwnsOne("SVSim.Database.Models.CardCollectionInfo", "CollectionInfo", b1 => + { + b1.Property("ShadowverseCardEntryId") + .HasColumnType("bigint"); + + b1.Property("CraftCost") + .HasColumnType("integer"); + + b1.Property("DustReward") + .HasColumnType("integer"); + + b1.HasKey("ShadowverseCardEntryId"); + + b1.ToTable("Cards"); + + b1.WithOwner() + .HasForeignKey("ShadowverseCardEntryId"); + }); + + b.Navigation("Class"); + + b.Navigation("CollectionInfo"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin") + .WithMany() + .HasForeignKey("LeaderSkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.SleeveEntry", "Sleeve") + .WithMany() + .HasForeignKey("SleeveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Decks") + .HasForeignKey("ViewerId"); + + b.OwnsMany("SVSim.Database.Models.DeckCard", "Cards", b1 => + { + b1.Property("ShadowverseDeckEntryId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.HasKey("ShadowverseDeckEntryId", "Id"); + + b1.HasIndex("CardId"); + + b1.ToTable("DeckCard"); + + b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ShadowverseDeckEntryId"); + + b1.Navigation("Card"); + }); + + b.Navigation("Cards"); + + b.Navigation("Class"); + + b.Navigation("LeaderSkin"); + + b.Navigation("Sleeve"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b => + { + b.HasOne("SVSim.Database.Models.SleeveShopSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.SleeveShopProductRewardEntry", "Rewards", b1 => + { + b1.Property("SleeveShopProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("SleeveShopProductEntryId", "Id"); + + b1.ToTable("SleeveShopProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("SleeveShopProductEntryId"); + }); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsMany("SVSim.Database.Models.MyPageBgRotationEntry", "MyPageBgRotation", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Slot") + .HasColumnType("integer"); + + b1.Property("BgId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Slot"); + + b1.ToTable("ViewerMyPageBgRotation", (string)null); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.Property("IsProtected") + .HasColumnType("boolean"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("CardId"); + + b1.HasIndex("ViewerId", "CardId") + .IsUnique(); + + b1.ToTable("OwnedCardEntry"); + + b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + + b1.Navigation("Card"); + }); + + b.OwnsMany("SVSim.Database.Models.OwnedItemEntry", "Items", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.Property("ItemId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ItemId"); + + b1.HasIndex("ViewerId", "ItemId") + .IsUnique(); + + b1.ToTable("OwnedItemEntry"); + + b1.HasOne("SVSim.Database.Models.ItemEntry", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Item"); + + b1.Navigation("Viewer"); + }); + + b.OwnsMany("SVSim.Database.Models.SocialAccountConnection", "SocialAccountConnections", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AccountId") + .HasColumnType("numeric(20,0)"); + + b1.Property("AccountType") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("AccountType", "AccountId") + .IsUnique(); + + b1.ToTable("SocialAccountConnection"); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Viewer"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerBuildDeckProductPurchase", "BuildDeckPurchases", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ProductId") + .HasColumnType("integer"); + + b1.Property("PurchaseCount") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "ProductId") + .IsUnique(); + + b1.ToTable("ViewerBuildDeckProductPurchase"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerClassData", "Classes", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ClassId") + .HasColumnType("integer"); + + b1.Property("Exp") + .HasColumnType("integer"); + + b1.Property("IsRandomLeaderSkin") + .HasColumnType("boolean"); + + b1.Property("LeaderSkinId") + .HasColumnType("integer"); + + b1.Property("Level") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ClassId"); + + b1.HasIndex("LeaderSkinId"); + + b1.ToTable("ViewerClassData"); + + b1.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin") + .WithMany() + .HasForeignKey("LeaderSkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Class"); + + b1.Navigation("LeaderSkin"); + + b1.Navigation("Viewer"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerCurrency", "Currency", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("AndroidCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("Crystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("DmmCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("FreeCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("IosCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("LifeTotalCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("RedEther") + .HasColumnType("numeric(20,0)"); + + b1.Property("Rupees") + .HasColumnType("numeric(20,0)"); + + b1.Property("SpotPoints") + .HasColumnType("numeric(20,0)"); + + b1.Property("SteamCrystals") + .HasColumnType("numeric(20,0)"); + + b1.HasKey("ViewerId"); + + b1.ToTable("Viewers"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerFreePackClaim", "FreePackClaims", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("FreeGachaCampaignId") + .HasColumnType("integer"); + + b1.Property("ClaimCount") + .HasColumnType("integer"); + + b1.Property("LastClaimedAt") + .HasColumnType("timestamp with time zone"); + + b1.HasKey("ViewerId", "FreeGachaCampaignId"); + + b1.ToTable("ViewerFreePackClaim"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerGachaPointBalance", "GachaPointBalances", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.Property("Points") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "PackId") + .IsUnique(); + + b1.ToTable("ViewerGachaPointBalance"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerGachaPointReceived", "GachaPointReceived", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "PackId", "CardId") + .IsUnique(); + + b1.ToTable("ViewerGachaPointReceived"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerInfo", "Info", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b1.Property("ChallengeTwoPickSleeveId") + .HasColumnType("bigint"); + + b1.Property("CountryCode") + .IsRequired() + .HasColumnType("text"); + + b1.Property("IsFoilPreferred") + .HasColumnType("boolean"); + + b1.Property("IsOfficial") + .HasColumnType("boolean"); + + b1.Property("IsOfficialMarkDisplayed") + .HasColumnType("boolean"); + + b1.Property("IsPrizePreferred") + .HasColumnType("boolean"); + + b1.Property("IsSkipGachaEffect") + .HasColumnType("boolean"); + + b1.Property("MaxFriends") + .HasColumnType("integer"); + + b1.Property("SelectedDegreeId") + .HasColumnType("integer"); + + b1.Property("SelectedEmblemId") + .HasColumnType("integer"); + + b1.Property("UseChallengeTwoPickPremiumCard") + .HasColumnType("boolean"); + + b1.HasKey("ViewerId"); + + b1.HasIndex("SelectedDegreeId"); + + b1.HasIndex("SelectedEmblemId"); + + b1.ToTable("Viewers"); + + b1.HasOne("SVSim.Database.Models.DegreeEntry", "SelectedDegree") + .WithMany() + .HasForeignKey("SelectedDegreeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.HasOne("SVSim.Database.Models.EmblemEntry", "SelectedEmblem") + .WithMany() + .HasForeignKey("SelectedEmblemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + + b1.Navigation("SelectedDegree"); + + b1.Navigation("SelectedEmblem"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerMissionData", "MissionData", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("HasReceivedPickTwoMission") + .HasColumnType("boolean"); + + b1.Property("MissionChangeTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("MissionReceiveType") + .HasColumnType("integer"); + + b1.Property("TutorialState") + .HasColumnType("integer"); + + b1.HasKey("ViewerId"); + + b1.ToTable("Viewers"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerPackOpenCount", "PackOpenCounts", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("LastDailyFreeAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("OpenCount") + .HasColumnType("integer"); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.ToTable("ViewerPackOpenCount"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.Navigation("BuildDeckPurchases"); + + b.Navigation("Cards"); + + b.Navigation("Classes"); + + b.Navigation("Currency") + .IsRequired(); + + b.Navigation("FreePackClaims"); + + b.Navigation("GachaPointBalances"); + + b.Navigation("GachaPointReceived"); + + b.Navigation("Guild"); + + b.Navigation("Info") + .IsRequired(); + + b.Navigation("Items"); + + b.Navigation("MissionData") + .IsRequired(); + + b.Navigation("MyPageBgRotation"); + + b.Navigation("PackOpenCounts"); + + b.Navigation("SocialAccountConnections"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Achievements") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAcquireHistoryEntry", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("EventCounters") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerFriend", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("FriendViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("OwnerViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerFriendApply", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("FromViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ToViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Missions") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPlayedTogether", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("OwnerViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPresent", b => + { + b.HasOne("SVSim.Database.Models.Viewer", "Viewer") + .WithMany() + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Viewer"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerSerialCodeRedemption", b => + { + b.HasOne("SVSim.Database.Models.SerialCodeEntry", null) + .WithMany() + .HasForeignKey("SerialCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SleeveEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.SleeveEntry", null) + .WithMany() + .HasForeignKey("SleevesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Guild.Guild", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassSeasonEntry", b => + { + b.Navigation("Rewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b => + { + b.Navigation("LeaderSkins"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b => + { + b.Navigation("Puzzles"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SerialCodeEntry", b => + { + b.Navigation("Rewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Navigation("Achievements"); + + b.Navigation("Decks"); + + b.Navigation("EventCounters"); + + b.Navigation("Missions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.cs b/SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.cs new file mode 100644 index 00000000..bcd8d6e5 --- /dev/null +++ b/SVSim.Database/Migrations/20260627202442_AddGuildLeaderViewerIdFk.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + /// + public partial class AddGuildLeaderViewerIdFk : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LeaderViewerId", + table: "Guilds", + type: "bigint", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.CreateIndex( + name: "IX_Guilds_LeaderViewerId", + table: "Guilds", + column: "LeaderViewerId"); + + migrationBuilder.AddForeignKey( + name: "FK_Guilds_Viewers_LeaderViewerId", + table: "Guilds", + column: "LeaderViewerId", + principalTable: "Viewers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Guilds_Viewers_LeaderViewerId", + table: "Guilds"); + + migrationBuilder.DropIndex( + name: "IX_Guilds_LeaderViewerId", + table: "Guilds"); + + migrationBuilder.AlterColumn( + name: "LeaderViewerId", + table: "Guilds", + type: "bigint", + nullable: false, + defaultValue: 0L, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + } + } +} diff --git a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs index 49a3179c..a558edd9 100644 --- a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs +++ b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs @@ -110,7 +110,7 @@ namespace SVSim.Database.Migrations b.Property("JoinCondition") .HasColumnType("integer"); - b.Property("LeaderViewerId") + b.Property("LeaderViewerId") .HasColumnType("bigint"); b.Property("Name") @@ -120,6 +120,8 @@ namespace SVSim.Database.Migrations b.HasKey("GuildId"); + b.HasIndex("LeaderViewerId"); + b.HasIndex("Name"); b.ToTable("Guilds"); @@ -3653,6 +3655,14 @@ namespace SVSim.Database.Migrations .IsRequired(); }); + modelBuilder.Entity("SVSim.Database.Entities.Guild.Guild", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("LeaderViewerId") + .OnDelete(DeleteBehavior.SetNull); + }); + modelBuilder.Entity("SVSim.Database.Entities.Guild.GuildChatMessage", b => { b.HasOne("SVSim.Database.Entities.Guild.Guild", "Guild") diff --git a/SVSim.Database/Repositories/Guild/GuildMemberRepository.cs b/SVSim.Database/Repositories/Guild/GuildMemberRepository.cs index f7e7b48b..e993c80d 100644 --- a/SVSim.Database/Repositories/Guild/GuildMemberRepository.cs +++ b/SVSim.Database/Repositories/Guild/GuildMemberRepository.cs @@ -70,4 +70,15 @@ public sealed class GuildMemberRepository : IGuildMemberRepository result[row.GuildId] = row.Count; return result; } + + public async Task> GetViewerIdsInAGuildAsync( + IReadOnlyList viewerIds, CancellationToken ct = default) + { + if (viewerIds.Count == 0) return new HashSet(); + var ids = await _db.GuildMembers + .Where(m => viewerIds.Contains(m.ViewerId)) + .Select(m => m.ViewerId) + .ToListAsync(ct); + return new HashSet(ids); + } } diff --git a/SVSim.Database/Repositories/Guild/IGuildMemberRepository.cs b/SVSim.Database/Repositories/Guild/IGuildMemberRepository.cs index 75243808..abb09503 100644 --- a/SVSim.Database/Repositories/Guild/IGuildMemberRepository.cs +++ b/SVSim.Database/Repositories/Guild/IGuildMemberRepository.cs @@ -15,4 +15,11 @@ public interface IGuildMemberRepository /// Returns a count of members per guild for the given guild ids. Used by Task 9 search. Task> CountBatchByGuildIdsAsync(IReadOnlyList guildIds, CancellationToken ct = default); + + /// + /// Batch-checks whether each viewer id in is currently in any guild. + /// Returns a set of viewer ids that ARE in a guild. Used by /guild/friend_list to set + /// is_join_guild. + /// + Task> GetViewerIdsInAGuildAsync(IReadOnlyList viewerIds, CancellationToken ct = default); } diff --git a/SVSim.Database/Repositories/Viewer/IViewerRepository.cs b/SVSim.Database/Repositories/Viewer/IViewerRepository.cs index 7c186028..da2e98ca 100644 --- a/SVSim.Database/Repositories/Viewer/IViewerRepository.cs +++ b/SVSim.Database/Repositories/Viewer/IViewerRepository.cs @@ -13,6 +13,18 @@ public record ChatUserProfile( int Rank, int DegreeId); +/// +/// Richer profile shape used by guild management surfaces (invite list, join-request list). +/// Extends with IsOfficialMarkDisplayed. +/// +public record GuildMemberProfile( + string Name, + long EmblemId, + string CountryCode, + int Rank, + int DegreeId, + bool IsOfficialMarkDisplayed); + public interface IViewerRepository { Task GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId); @@ -57,4 +69,23 @@ public interface IViewerRepository /// in the controller. Ids with no matching row are absent from the result. Read-only (AsNoTracking). /// Task> LoadChatProfilesAsync(IReadOnlyCollection viewerIds, CancellationToken ct = default); + + /// + /// Batch-loads the fields (emblem, degree, + /// IsOfficialMarkDisplayed) for a set of viewer ids. Used by guild management surfaces + /// (invite_user_list, join_request_list). Ids with no matching row are absent. Read-only. + /// + Task> LoadGuildProfileBatchAsync(IReadOnlyCollection viewerIds, CancellationToken ct = default); + + /// + /// Loads a viewer's currently-equipped emblem id, or 100_000_000 (default) if none. + /// Used by to seed the guild's initial emblem. + /// Returns 100_000_000 if the viewer doesn't exist. + /// + Task GetEquippedEmblemIdAsync(long viewerId, CancellationToken ct = default); + + /// + /// Batch-loads owned guild-emblem ids for a viewer. Used by /guild/emblem_list. + /// + Task> GetEmblemListAsync(long viewerId, CancellationToken ct = default); } diff --git a/SVSim.Database/Repositories/Viewer/ViewerRepository.cs b/SVSim.Database/Repositories/Viewer/ViewerRepository.cs index 44731c5b..a0656cd2 100644 --- a/SVSim.Database/Repositories/Viewer/ViewerRepository.cs +++ b/SVSim.Database/Repositories/Viewer/ViewerRepository.cs @@ -323,6 +323,56 @@ public class ViewerRepository : IViewerRepository DegreeId: r.DegreeId)); } + public async Task> LoadGuildProfileBatchAsync(IReadOnlyCollection viewerIds, CancellationToken ct = default) + { + if (viewerIds.Count == 0) return new Dictionary(); + var rows = await _dbContext.Set() + .AsNoTracking() + .Include(v => v.Info.SelectedEmblem) + .Include(v => v.Info.SelectedDegree) + .Where(v => viewerIds.Contains(v.Id)) + .Select(v => new + { + v.Id, + v.DisplayName, + EmblemId = v.Info.SelectedEmblem != null ? v.Info.SelectedEmblem.Id : 100_000_000L, + CountryCode = v.Info.CountryCode ?? "", + DegreeId = v.Info.SelectedDegree != null ? (int)v.Info.SelectedDegree.Id : 0, + IsOfficialMarkDisplayed = v.Info.IsOfficialMarkDisplayed, + }) + .ToListAsync(ct); + return rows.ToDictionary( + r => r.Id, + r => new GuildMemberProfile( + Name: r.DisplayName, + EmblemId: r.EmblemId, + CountryCode: r.CountryCode, + Rank: 1, // TODO: real rank when rank tracking lands + DegreeId: r.DegreeId, + IsOfficialMarkDisplayed: r.IsOfficialMarkDisplayed)); + } + + public async Task GetEquippedEmblemIdAsync(long viewerId, CancellationToken ct = default) + { + var row = await _dbContext.Set() + .AsNoTracking() + .Include(v => v.Info.SelectedEmblem) + .Where(v => v.Id == viewerId) + .Select(v => new { EmblemId = v.Info.SelectedEmblem != null ? v.Info.SelectedEmblem.Id : 100_000_000L }) + .FirstOrDefaultAsync(ct); + return row?.EmblemId ?? 100_000_000L; + } + + public async Task> GetEmblemListAsync(long viewerId, CancellationToken ct = default) + { + var viewer = await _dbContext.Set() + .AsNoTracking() + .Include(v => v.Emblems) + .Where(v => v.Id == viewerId) + .FirstOrDefaultAsync(ct); + return viewer?.Emblems.Select(e => (long)e.Id).ToList() ?? new List(); + } + private async Task BuildDefaultViewer(string displayName, int initialTutorialState = 1) { Models.Viewer viewer = new Models.Viewer diff --git a/SVSim.Database/SVSimDbContext.cs b/SVSim.Database/SVSimDbContext.cs index bcc03ba2..99310e4b 100644 --- a/SVSim.Database/SVSimDbContext.cs +++ b/SVSim.Database/SVSimDbContext.cs @@ -520,6 +520,9 @@ public class SVSimDbContext : DbContext e.Property(g => g.Name).HasMaxLength(64); e.Property(g => g.Description).HasMaxLength(512); e.HasIndex(g => g.Name); // name search + e.HasOne().WithMany() + .HasForeignKey(g => g.LeaderViewerId) + .OnDelete(DeleteBehavior.SetNull); }); modelBuilder.Entity(e => diff --git a/SVSim.Database/Services/Guild/GuildService.cs b/SVSim.Database/Services/Guild/GuildService.cs index 5efc540f..1df90ad2 100644 --- a/SVSim.Database/Services/Guild/GuildService.cs +++ b/SVSim.Database/Services/Guild/GuildService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using SVSim.Database.Entities.Guild; using SVSim.Database.Models.Config; using SVSim.Database.Repositories.Guild; @@ -67,9 +68,9 @@ public sealed class GuildService : IGuildService var rows = await _guilds.SearchAsync(name ?? "", activity, joinCondition, memberBucket, cfg.MaxMemberNum, cfg.SearchResultCap, ct); var guildIds = rows.Select(r => r.GuildId).ToList(); var memberCounts = await _members.CountBatchByGuildIdsAsync(guildIds, ct); - var leaderIds = rows.Select(r => r.LeaderViewerId).Distinct().ToList(); + var leaderIds = rows.Select(r => r.LeaderViewerId).Where(id => id.HasValue).Select(id => id!.Value).Distinct().ToList(); var leaderNames = await _viewers.LoadDisplayNamesAsync(leaderIds, ct); - return rows.Select(r => new GuildSearchEntry(r, memberCounts.GetValueOrDefault(r.GuildId, 0), leaderNames.GetValueOrDefault(r.LeaderViewerId, ""))).ToList(); + return rows.Select(r => new GuildSearchEntry(r, memberCounts.GetValueOrDefault(r.GuildId, 0), leaderNames.GetValueOrDefault(r.LeaderViewerId ?? 0L, ""))).ToList(); } public async Task CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default) @@ -86,10 +87,9 @@ public sealed class GuildService : IGuildService if (await _guilds.NameExistsAsync(req.Name, ct)) return new(GuildOpResultCode.NameTaken); - var viewer = await _db.Viewers - .Include(v => v.Info.SelectedEmblem) - .FirstOrDefaultAsync(v => v.Id == viewerId, ct); - if (viewer is null) return new(GuildOpResultCode.PermissionDenied); + // Load the viewer's equipped emblem to use as the guild's initial emblem. + // If the viewer doesn't exist (shouldn't happen post-auth), returns 100_000_000 default. + long viewerEmblemId = await _viewers.GetEquippedEmblemIdAsync(viewerId, ct); var id = await _idGen.NextAsync( (cand, c) => _guilds.GetByIdAsync(cand, c).ContinueWith(t => t.Result is not null, c), @@ -101,7 +101,7 @@ public sealed class GuildService : IGuildService Name = req.Name, Description = "", LeaderViewerId = viewerId, - EmblemId = DefaultEmblemId(viewer), + EmblemId = viewerEmblemId, Activity = (GuildActivity)req.Activity, JoinCondition = (GuildJoinCondition)req.JoinCondition, CreatedAt = now, @@ -116,22 +116,12 @@ public sealed class GuildService : IGuildService await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct); // Update Viewer.GuildId pointer for quick lookups. - viewer.GuildId = id; - await _db.SaveChangesAsync(ct); + await _viewers.SetGuildIdAsync(viewerId, id, ct); await _chat.EmitSystemEventAsync(id, viewerId, GuildChatMessageType.CreateGuild, body: null, ct); return new(GuildOpResultCode.Ok, GuildId: id); } - /// - /// Returns the viewer's currently-equipped emblem id, or 100000000 (default) if none. - /// - private static long DefaultEmblemId(Models.Viewer viewer) - { - var emblemId = viewer.Info?.SelectedEmblem?.Id; - return emblemId is > 0 ? emblemId.Value : 100_000_000L; - } - public async Task UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default) { var m = await _members.GetMembershipAsync(viewerId, ct); @@ -177,17 +167,33 @@ public sealed class GuildService : IGuildService if (membership is null) return new(GuildOpResultCode.NotInGuild); if (membership.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); - var now = DateTime.UtcNow; - var members = await _members.ListByGuildAsync(membership.GuildId, ct); - foreach (var m in members) await _viewers.ClearGuildIdAsync(m.ViewerId, ct); + IDbContextTransaction? tx = null; + try + { + try { tx = await _db.Database.BeginTransactionAsync(ct); } catch (InvalidOperationException) { /* InMemory provider */ } - await _chatMessages.DeleteAllForGuildAsync(membership.GuildId, ct); - await _invites.DeleteAllForGuildAsync(membership.GuildId, ct); - await _joinRequests.DeleteAllForGuildAsync(membership.GuildId, ct); - await _members.DeleteAllForGuildAsync(membership.GuildId, ct); - await _guilds.MarkBrokenUpAsync(membership.GuildId, now, ct); + var now = DateTime.UtcNow; + var members = await _members.ListByGuildAsync(membership.GuildId, ct); + foreach (var m in members) await _viewers.ClearGuildIdAsync(m.ViewerId, ct); - return GuildOpResult.Ok; + await _chatMessages.DeleteAllForGuildAsync(membership.GuildId, ct); + await _invites.DeleteAllForGuildAsync(membership.GuildId, ct); + await _joinRequests.DeleteAllForGuildAsync(membership.GuildId, ct); + await _members.DeleteAllForGuildAsync(membership.GuildId, ct); + await _guilds.MarkBrokenUpAsync(membership.GuildId, now, ct); + + if (tx != null) await tx.CommitAsync(ct); + return GuildOpResult.Ok; + } + catch + { + if (tx != null) await tx.RollbackAsync(ct); + throw; + } + finally + { + tx?.Dispose(); + } } public async Task InviteAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default) @@ -305,12 +311,31 @@ public sealed class GuildService : IGuildService private async Task CommitJoinAsync(long viewerId, int guildId, DateTime now, CancellationToken ct) { - await _members.AddAsync( - new GuildMember { GuildId = guildId, ViewerId = viewerId, Role = GuildRole.Regular, JoinedAt = now }, - ct); - await _viewers.SetGuildIdAsync(viewerId, guildId, ct); - await _invites.ConsumePendingForViewerAsync(viewerId, now, ct); - await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct); + IDbContextTransaction? tx = null; + try + { + try { tx = await _db.Database.BeginTransactionAsync(ct); } catch (InvalidOperationException) { /* InMemory provider */ } + + await _members.AddAsync( + new GuildMember { GuildId = guildId, ViewerId = viewerId, Role = GuildRole.Regular, JoinedAt = now }, + ct); + await _viewers.SetGuildIdAsync(viewerId, guildId, ct); + await _invites.ConsumePendingForViewerAsync(viewerId, now, ct); + await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct); + + if (tx != null) await tx.CommitAsync(ct); + } + catch + { + if (tx != null) await tx.RollbackAsync(ct); + throw; + } + finally + { + tx?.Dispose(); + } + + // Emit chat event outside the transaction — chat failure doesn't roll back the join. await _chat.EmitSystemEventAsync(guildId, viewerId, GuildChatMessageType.Join, body: null, ct); } @@ -470,28 +495,20 @@ public sealed class GuildService : IGuildService if (pendingInvites.Count == 0) return Array.Empty(); var inviteeIds = pendingInvites.Select(i => i.InviteeViewerId).Distinct().ToList(); - var names = await _viewers.LoadDisplayNamesAsync(inviteeIds, ct); - - // Load emblem + degree data for invitees. - var viewerRows = await _db.Viewers - .AsNoTracking() - .Include(v => v.Info.SelectedEmblem) - .Include(v => v.Info.SelectedDegree) - .Where(v => inviteeIds.Contains(v.Id)) - .ToDictionaryAsync(v => v.Id, ct); + var profiles = await _viewers.LoadGuildProfileBatchAsync(inviteeIds, ct); var result = new List(pendingInvites.Count); foreach (var invite in pendingInvites) { - viewerRows.TryGetValue(invite.InviteeViewerId, out var v); + profiles.TryGetValue(invite.InviteeViewerId, out var p); result.Add(new GuildOutgoingInviteEntry( InviteId: invite.Id, InviteeViewerId: invite.InviteeViewerId, - Name: names.GetValueOrDefault(invite.InviteeViewerId, ""), - EmblemId: v?.Info?.SelectedEmblem?.Id is > 0 ? v.Info.SelectedEmblem.Id : 100_000_000L, - CountryCode: v?.Info?.CountryCode ?? "", - Rank: 1, - DegreeId: v?.Info?.SelectedDegree?.Id ?? 0, + Name: p?.Name ?? "", + EmblemId: p?.EmblemId ?? 100_000_000L, + CountryCode: p?.CountryCode ?? "", + Rank: p?.Rank ?? 1, + DegreeId: p?.DegreeId ?? 0, CreatedAt: invite.CreatedAt)); } return result; @@ -505,14 +522,14 @@ public sealed class GuildService : IGuildService var guildIds = pendingInvites.Select(i => i.GuildId).Distinct().ToList(); var memberCounts = await _members.CountBatchByGuildIdsAsync(guildIds, ct); - var leaderIds = pendingInvites.Select(i => i.Guild.LeaderViewerId).Distinct().ToList(); + var leaderIds = pendingInvites.Select(i => i.Guild.LeaderViewerId).Where(id => id.HasValue).Select(id => id!.Value).Distinct().ToList(); var leaderNames = await _viewers.LoadDisplayNamesAsync(leaderIds, ct); return pendingInvites.Select(invite => new GuildReceivedInviteEntry( InviteId: invite.Id, Guild: invite.Guild, MemberNum: memberCounts.GetValueOrDefault(invite.GuildId, 0), - LeaderName: leaderNames.GetValueOrDefault(invite.Guild.LeaderViewerId, ""))).ToList(); + LeaderName: leaderNames.GetValueOrDefault(invite.Guild.LeaderViewerId ?? 0L, ""))).ToList(); } public async Task> ListPendingJoinRequestsForMyGuildAsync(long viewerId, CancellationToken ct = default) @@ -527,26 +544,19 @@ public sealed class GuildService : IGuildService if (requests.Count == 0) return Array.Empty(); var applicantIds = requests.Select(r => r.ViewerId).Distinct().ToList(); - var names = await _viewers.LoadDisplayNamesAsync(applicantIds, ct); - - var viewerRows = await _db.Viewers - .AsNoTracking() - .Include(v => v.Info.SelectedEmblem) - .Include(v => v.Info.SelectedDegree) - .Where(v => applicantIds.Contains(v.Id)) - .ToDictionaryAsync(v => v.Id, ct); + var profiles = await _viewers.LoadGuildProfileBatchAsync(applicantIds, ct); return requests.Select(r => { - viewerRows.TryGetValue(r.ViewerId, out var v); + profiles.TryGetValue(r.ViewerId, out var p); return new GuildJoinRequestEntry( ApplicantViewerId: r.ViewerId, - Name: names.GetValueOrDefault(r.ViewerId, ""), - EmblemId: v?.Info?.SelectedEmblem?.Id is > 0 ? v.Info.SelectedEmblem.Id : 100_000_000L, - CountryCode: v?.Info?.CountryCode ?? "", - Rank: 1, - DegreeId: v?.Info?.SelectedDegree?.Id ?? 0, - IsOfficialMarkDisplayed: v?.Info?.IsOfficialMarkDisplayed == true, + Name: p?.Name ?? "", + EmblemId: p?.EmblemId ?? 100_000_000L, + CountryCode: p?.CountryCode ?? "", + Rank: p?.Rank ?? 1, + DegreeId: p?.DegreeId ?? 0, + IsOfficialMarkDisplayed: p?.IsOfficialMarkDisplayed ?? false, RequestedAt: r.CreatedAt); }).ToList(); } diff --git a/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs b/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs index 589cbee0..dc319692 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs @@ -131,15 +131,19 @@ public sealed class GuildChatController : SVSimController } [HttpPost("replay_detail")] - public async Task> ReplayDetail([FromBody] GuildChatReplayDetailRequest req, CancellationToken ct) + public async Task ReplayDetail([FromBody] GuildChatReplayDetailRequest req, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var payloadJson = await _chat.GetReplayDetailAsync(viewerId, (int)req.MessageId, ct); if (payloadJson is null) return Ok(new { result_code = 2 }); - var replayInfo = ParseJsonElementOrNull(payloadJson); - return new GuildChatReplayDetailResponse { ReplayInfo = replayInfo }; + // ReplayDetailInfo(data) accesses data["battleId"], data["seed"], data["vid1"], etc. + // directly without Keys.Contains guards — returning a wrapper object crashes the client. + // The stored ReplayPayload IS the full flat battle object; emit its fields directly as + // the data payload so data["battleId"].ToLong() etc. resolve correctly. + var replayElement = ParseJsonElementOrNull(payloadJson); + return replayElement.HasValue ? Ok(replayElement.Value) : Ok(new { result_code = 2 }); } [HttpPost("deck_log")] diff --git a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs index e3794a88..77b1f478 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using SVSim.Database; using SVSim.Database.Entities.Guild; -using SVSim.Database.Models; using SVSim.Database.Models.Config; +using SVSim.Database.Repositories.Guild; +using SVSim.Database.Repositories.Viewer; using SVSim.Database.Services; using SVSim.Database.Services.Friend; using SVSim.Database.Services.Guild; @@ -20,17 +19,24 @@ public sealed class GuildController : SVSimController { private readonly IGuildService _guild; private readonly IGameConfigService _configs; - private readonly SVSimDbContext _db; + private readonly IViewerRepository _viewers; + private readonly IGuildMemberRepository _members; private readonly IFriendService _friends; // Wire error code returned when guild operations fail (non-1 result_code). private const int GuildErrorResultCode = 2; - public GuildController(IGuildService guild, IGameConfigService configs, SVSimDbContext db, IFriendService friends) + public GuildController( + IGuildService guild, + IGameConfigService configs, + IViewerRepository viewers, + IGuildMemberRepository members, + IFriendService friends) { _guild = guild; _configs = configs; - _db = db; + _viewers = viewers; + _members = members; _friends = friends; } @@ -56,7 +62,7 @@ public sealed class GuildController : SVSimController resp.JoinRequestCount = view.JoinRequestCount; resp.InviteCount = view.InviteCount; var memberDtos = await ToMemberDtoListAsync(view.Members, viewerId, ct); - var leaderName = memberDtos.FirstOrDefault(m => m.ViewerId == view.Guild.LeaderViewerId)?.Name ?? ""; + var leaderName = memberDtos.FirstOrDefault(m => m.ViewerId == (view.Guild.LeaderViewerId ?? 0L))?.Name ?? ""; resp.Guild = new GuildBundle { Detail = ToDetailDto(view.Guild, view.Members.Count, leaderName), @@ -95,14 +101,17 @@ public sealed class GuildController : SVSimController var r = await _guild.UpdateAsync(viewerId, new(activity, joinCondition, name), ct); if (!r.IsOk) return WireError(); - var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); + var m = await _members.GetMembershipAsync(viewerId, ct); if (m is null) return WireError(); var guild = await _guild.GetActiveAsync(m.GuildId, ct); if (guild is null) return WireError(); - var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct); - var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); + var memberCount = await _members.CountByGuildAsync(guild.GuildId, ct); + var leaderNames = guild.LeaderViewerId.HasValue + ? await _viewers.LoadDisplayNamesAsync(new[] { guild.LeaderViewerId.Value }, ct) + : new Dictionary(); + var leaderName = leaderNames.GetValueOrDefault(guild.LeaderViewerId ?? 0L, ""); // GuildUpdateTask.Parse() reads data["guild"] directly as GuildDetailInfo — flat, no "detail" wrapper. - return new GuildUpdateResponse { Guild = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") }; + return new GuildUpdateResponse { Guild = ToDetailDto(guild, memberCount, leaderName) }; } [HttpPost("update_description")] @@ -120,16 +129,19 @@ public sealed class GuildController : SVSimController var r = await _guild.UpdateEmblemAsync(viewerId, req.EmblemId, ct); if (!r.IsOk) return WireError(); - var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); + var m = await _members.GetMembershipAsync(viewerId, ct); if (m is null) return WireError(); var guild = await _guild.GetActiveAsync(m.GuildId, ct); if (guild is null) return WireError(); - var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct); - var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); + var memberCount = await _members.CountByGuildAsync(guild.GuildId, ct); + var leaderNames2 = guild.LeaderViewerId.HasValue + ? await _viewers.LoadDisplayNamesAsync(new[] { guild.LeaderViewerId.Value }, ct) + : new Dictionary(); + var leaderName2 = leaderNames2.GetValueOrDefault(guild.LeaderViewerId ?? 0L, ""); // GuildEmblemUpdateTask.Parse() reads data["guild"]["detail"] — nested wrapper required. return new GuildUpdateEmblemResponse { - Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") } + Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leaderName2) } }; } @@ -145,14 +157,10 @@ public sealed class GuildController : SVSimController public async Task> EmblemList([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); - var viewer = await _db.Viewers - .AsNoTracking() - .Include(v => v.Emblems) - .FirstOrDefaultAsync(v => v.Id == viewerId, ct); - var emblems = viewer?.Emblems ?? new(); + var emblemIds = await _viewers.GetEmblemListAsync(viewerId, ct); return new GuildEmblemListResponse { - EmblemList = emblems.Select(e => new GuildEmblemEntry { EmblemId = e.Id }).ToList() + EmblemList = emblemIds.Select(id => new GuildEmblemEntry { EmblemId = id }).ToList() }; } @@ -162,13 +170,16 @@ public sealed class GuildController : SVSimController if (!TryGetViewerId(out _)) return Unauthorized(); var guild = await _guild.GetActiveAsync(req.GuildId, ct); if (guild is null) return new GuildOthersInfoResponse(); - var memberCount = await _db.GuildMembers.CountAsync(m => m.GuildId == guild.GuildId, ct); - var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); + var memberCount = await _members.CountByGuildAsync(guild.GuildId, ct); + var othersLeaderNames = guild.LeaderViewerId.HasValue + ? await _viewers.LoadDisplayNamesAsync(new[] { guild.LeaderViewerId.Value }, ct) + : new Dictionary(); + var othersLeaderName = othersLeaderNames.GetValueOrDefault(guild.LeaderViewerId ?? 0L, ""); return new GuildOthersInfoResponse { Guild = new GuildDetailSubTree { - Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") + Detail = ToDetailDto(guild, memberCount, othersLeaderName) } }; } @@ -184,16 +195,11 @@ public sealed class GuildController : SVSimController // For each friend, determine if they are already in a guild (is_join_guild). var friendViewerIds = friendInfo.Friends.Select(f => (long)f.ViewerId).ToList(); - var viewerRows = await _db.Viewers - .AsNoTracking() - .Where(v => friendViewerIds.Contains(v.Id)) - .Select(v => new { v.Id, v.GuildId }) - .ToDictionaryAsync(v => v.Id, v => v.GuildId, ct); + var inGuild = await _members.GetViewerIdsInAGuildAsync(friendViewerIds, ct); var candidates = new List(friendInfo.Friends.Count); foreach (var f in friendInfo.Friends) { - viewerRows.TryGetValue(f.ViewerId, out var guildId); candidates.Add(new GuildInviteCandidateDto { ViewerId = f.ViewerId, @@ -202,7 +208,7 @@ public sealed class GuildController : SVSimController CountryCode = f.CountryCode, Rank = f.Rank, DegreeId = f.DegreeId, - IsJoinGuild = guildId is not null, + IsJoinGuild = inGuild.Contains(f.ViewerId), }); } @@ -247,7 +253,7 @@ public sealed class GuildController : SVSimController Activity = (int)e.Guild.Activity, MemberNum = e.MemberNum, LeaderName = e.LeaderName, - LeaderViewerId = e.Guild.LeaderViewerId, + LeaderViewerId = e.Guild.LeaderViewerId ?? 0L, InviteId = e.InviteId, }).ToList(), }; @@ -357,12 +363,10 @@ public sealed class GuildController : SVSimController if (!r.IsOk) return WireError(); // Load caller's membership to get the guild id, then return the full updated member list. - var membership = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); + var membership = await _members.GetMembershipAsync(viewerId, ct); if (membership is null) return WireError(); - var members = await _db.GuildMembers - .Where(m => m.GuildId == membership.GuildId) - .ToListAsync(ct); + var members = await _members.ListByGuildAsync(membership.GuildId, ct); var memberDtos = await ToMemberDtoListAsync(members, viewerId, ct); return new GuildChangeRoleResponse { Members = memberDtos }; @@ -379,7 +383,7 @@ public sealed class GuildController : SVSimController JoinCondition = (int)guild.JoinCondition, Activity = (int)guild.Activity, MemberNum = memberCount, - LeaderViewerId = guild.LeaderViewerId, + LeaderViewerId = guild.LeaderViewerId ?? 0L, LeaderName = leaderName, }; @@ -391,33 +395,26 @@ public sealed class GuildController : SVSimController if (members.Count == 0) return new(); var viewerIds = members.Select(m => m.ViewerId).ToList(); - - // Batch-load viewer rows with Info + SelectedEmblem + SelectedDegree. - var viewers = await _db.Viewers - .AsNoTracking() - .Include(v => v.Info.SelectedEmblem) - .Include(v => v.Info.SelectedDegree) - .Where(v => viewerIds.Contains(v.Id)) - .ToDictionaryAsync(v => v.Id, ct); + var profiles = await _viewers.LoadGuildProfileBatchAsync(viewerIds, ct); var result = new List(members.Count); foreach (var m in members) { - viewers.TryGetValue(m.ViewerId, out var v); - result.Add(ToMemberDto(m, v)); + profiles.TryGetValue(m.ViewerId, out var p); + result.Add(ToMemberDto(m, p)); } return result; } - private static GuildMemberInfoDto ToMemberDto(GuildMember member, Viewer? viewer) => new() + private static GuildMemberInfoDto ToMemberDto(GuildMember member, GuildMemberProfile? profile) => new() { ViewerId = member.ViewerId, - Name = viewer?.DisplayName ?? "", - EmblemId = viewer?.Info?.SelectedEmblem?.Id is > 0 ? viewer.Info.SelectedEmblem.Id : 100_000_000L, - CountryCode = viewer?.Info?.CountryCode ?? "", - Rank = 1, // TODO: populate from actual rank data when rank tracking lands - DegreeId = viewer?.Info?.SelectedDegree?.Id ?? 0, - IsOfficialMarkDisplayed = viewer?.Info?.IsOfficialMarkDisplayed == true ? 1 : 0, + Name = profile?.Name ?? "", + EmblemId = profile?.EmblemId ?? 100_000_000L, + CountryCode = profile?.CountryCode ?? "", + Rank = profile?.Rank ?? 1, + DegreeId = profile?.DegreeId ?? 0, + IsOfficialMarkDisplayed = profile?.IsOfficialMarkDisplayed == true ? 1 : 0, Role = (int)member.Role, }; diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs index 8453aca4..d2832b91 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs @@ -1,22 +1,32 @@ -using MessagePack; -using System.Text.Json; using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; /// /// Response for POST /guild_chat/replay_detail. -/// Shape is opaque pending replay-subsystem documentation. -/// TODO(post-merge): enumerate fields from Wizard/ReplayDetailInfo.cs when replay subsystem is documented. -/// Using JsonElement pass-through so the stored ReplayPayload JSON is forwarded verbatim. +/// +/// Wire shape: the stored ReplayPayload JSON is forwarded FLAT as the entire data payload. +/// ChatReplayDetailTask.Parse() calls new ReplayDetailInfo(base.ResponseData["data"]) which +/// then accesses data["battleId"], data["seed"], data["vid1"], etc. directly (no Keys.Contains +/// guards on required fields). Wrapping the payload under a "replay_info" key would crash the +/// client. The controller returns the raw JsonElement directly via Ok(element). +/// +/// C1 decision: Option (b) is superseded — client CAN handle a full payload; the bug was the +/// wrapper key. The flat pass-through here is Option (a) variant: emit stored fields at data level. +/// If GetReplayDetailAsync returns null (no stored replay), the controller emits result_code=2 +/// to gate the client off unguarded parsing. /// -[MessagePackObject] +// NOTE: This DTO is unused at runtime — the controller returns ActionResult directly. It is kept +// as documentation of the wire contract and for future typed deserialization tests. public class GuildChatReplayDetailResponse { - /// - /// Opaque replay payload forwarded verbatim from the stored ReplayPayload JSON. - /// TODO(post-merge): type this properly once replay subsystem shape is captured. - /// - [JsonPropertyName("replay_info"), Key("replay_info")] - public JsonElement? ReplayInfo { get; set; } + // Fields are emitted flat at the data level, not under a wrapper key. + // See ReplayDetailInfo.cs in the decompiled client for the full field list. + // Key representative required fields (unguarded access in constructor): + // battleId, seed, fieldId, firstTurn, card_master_id, + // vid1/name1/charaId1/classId1/emblemId1/degreeId1/countryCode1/sleeveId1/battlePoint1/masterPoint1/rank1/isOfficial1/deck1 + // vid2/name2/charaId2/classId2/emblemId2/degreeId2/countryCode2/sleeveId2/battlePoint2/masterPoint2/rank2/isOfficial2/deck2 + + [JsonPropertyName("result_code")] + public int ResultCode { get; set; } = 1; } diff --git a/SVSim.UnitTests/Wire/GuildWireShape.cs b/SVSim.UnitTests/Wire/GuildWireShape.cs index a9efb2a6..28d74ae8 100644 --- a/SVSim.UnitTests/Wire/GuildWireShape.cs +++ b/SVSim.UnitTests/Wire/GuildWireShape.cs @@ -211,6 +211,40 @@ public class GuildWireShape "root must not be an object — no 'friends' wrapper allowed"); } + [Test] + public void ReplayDetail_response_passes_payload_fields_flat_not_wrapped() + { + // ChatReplayDetailTask.Parse() calls new ReplayDetailInfo(base.ResponseData["data"]). + // ReplayDetailInfo constructor accesses data["battleId"].ToLong(), data["seed"].ToInt(), etc. + // WITHOUT Keys.Contains guards. Wrapping under a "replay_info" key would crash the client. + // The controller emits the stored JSON element directly as the data payload — verify that + // the serialized shape does NOT have a wrapper key. + // + // Decompile evidence (ReplayDetailInfo.cs line 209-210): + // battle_id = data["battleId"].ToLong(); // unguarded + // seed = data["seed"].ToInt(); // unguarded + // Therefore: option (b) gate-off when payload is null; flat pass-through when it exists. + + // Simulate what the controller does: serialize the payload element directly. + const string storedPayloadJson = """{"battleId":"999","seed":42,"fieldId":1,"firstTurn":1,"card_master_id":100,"vid1":1,"name1":"A","charaId1":1,"classId1":1,"emblemId1":100000000,"degreeId1":0,"countryCode1":"JP","sleeveId1":3000011,"battlePoint1":0,"masterPoint1":0,"rank1":1,"isOfficial1":false,"deck1":[],"vid2":2,"name2":"B","charaId2":1,"classId2":2,"emblemId2":100000000,"degreeId2":0,"countryCode2":"JP","sleeveId2":3000011,"battlePoint2":0,"masterPoint2":0,"rank2":1,"isOfficial2":false,"deck2":[]}"""; + using var doc = JsonDocument.Parse(storedPayloadJson); + var element = doc.RootElement.Clone(); + + // Re-serialize as if returned from Ok(element) — should be flat + var json = JsonSerializer.Serialize(element, Opts); + using var resultDoc = JsonDocument.Parse(json); + var root = resultDoc.RootElement; + + // battleId must be at root level, NOT under a "replay_info" wrapper + Assert.That(root.TryGetProperty("battleId", out var battleId), Is.True, + "battleId must be at root level — no replay_info wrapper"); + Assert.That(battleId.GetString(), Is.EqualTo("999")); + + // replay_info key must NOT exist + Assert.That(root.TryGetProperty("replay_info", out _), Is.False, + "replay_info wrapper must NOT be present — client reads data[\"battleId\"] directly"); + } + [Test] public void GuildInfo_non_joined_serializes_to_prod_shape() {