From f707fb2ffbf436c5032e3d0dc0b1bcb30b49a83d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 15:32:33 -0400 Subject: [PATCH 1/9] refactor: drop unused DailyLoginBonuses table + importer The captured shape ({"1":[], "3":[], "4":[]}) is the empty-period wire echo; the client parser (LoadDetail.cs:553) only reads normal/total/ campaign. Table, entity, seed, and importer were never wired to a code path. Replaced by config-section catalog + service in the follow-up commits. --- .../Data/seeds/daily-login-bonus.json | 14 ------- .../Importers/DailyLoginBonusImporter.cs | 37 ------------------- .../Models/Seed/DailyLoginBonusSeed.cs | 11 ------ SVSim.Bootstrap/Program.cs | 1 - SVSim.Database/Models/DailyLoginBonusEntry.cs | 17 --------- .../Repositories/Globals/GlobalsRepository.cs | 3 -- .../Globals/IGlobalsRepository.cs | 1 - SVSim.Database/SVSimDbContext.cs | 1 - .../Controllers/LoadController.cs | 6 +-- .../Models/Dtos/Responses/IndexResponse.cs | 4 +- .../Importers/LoadIndexImporterTests.cs | 15 +------- .../Infrastructure/SVSimTestFactory.cs | 1 - .../Repositories/GlobalsRepositoryTests.cs | 10 ----- 13 files changed, 4 insertions(+), 117 deletions(-) delete mode 100644 SVSim.Bootstrap/Data/seeds/daily-login-bonus.json delete mode 100644 SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs delete mode 100644 SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs delete mode 100644 SVSim.Database/Models/DailyLoginBonusEntry.cs diff --git a/SVSim.Bootstrap/Data/seeds/daily-login-bonus.json b/SVSim.Bootstrap/Data/seeds/daily-login-bonus.json deleted file mode 100644 index e31694c2..00000000 --- a/SVSim.Bootstrap/Data/seeds/daily-login-bonus.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "id": 1, - "bonus_data": [] - }, - { - "id": 3, - "bonus_data": [] - }, - { - "id": 4, - "bonus_data": [] - } -] diff --git a/SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs b/SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs deleted file mode 100644 index 1c158f1f..00000000 --- a/SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore; -using SVSim.Bootstrap.Models.Seed; -using SVSim.Database; -using SVSim.Database.Models; - -namespace SVSim.Bootstrap.Importers; - -/// -/// Idempotent upsert of daily-login-bonus campaign rows from seeds/daily-login-bonus.json. -/// bonus_data array preserved verbatim — prod observed empty arrays outside active events. -/// -public class DailyLoginBonusImporter -{ - public async Task ImportAsync(SVSimDbContext context, string seedDir) - { - var seed = SeedLoader.LoadList(Path.Combine(seedDir, "daily-login-bonus.json")); - if (seed.Count == 0) return 0; - - var existing = await context.DailyLoginBonuses.ToDictionaryAsync(e => e.Id); - int created = 0, updated = 0; - foreach (var s in seed) - { - if (s.Id == 0) continue; - var entry = existing.TryGetValue(s.Id, out var ex) ? ex : new DailyLoginBonusEntry { Id = s.Id }; - entry.BonusData = s.BonusData.ValueKind == JsonValueKind.Undefined - ? "[]" - : JsonSerializer.Serialize(s.BonusData); - if (ex is null) { context.DailyLoginBonuses.Add(entry); existing[s.Id] = entry; created++; } - else updated++; - } - - await context.SaveChangesAsync(); - Console.WriteLine($"[DailyLoginBonusImporter] +{created}/~{updated}"); - return created + updated; - } -} diff --git a/SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs b/SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs deleted file mode 100644 index e64ed76d..00000000 --- a/SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace SVSim.Bootstrap.Models.Seed; - -/// Mirrors seeds/daily-login-bonus.json. bonus_data preserved verbatim. -public sealed class DailyLoginBonusSeed -{ - [JsonPropertyName("id")] public int Id { get; set; } - [JsonPropertyName("bonus_data")] public JsonElement BonusData { get; set; } -} diff --git a/SVSim.Bootstrap/Program.cs b/SVSim.Bootstrap/Program.cs index 629c91e4..5baccc63 100644 --- a/SVSim.Bootstrap/Program.cs +++ b/SVSim.Bootstrap/Program.cs @@ -94,7 +94,6 @@ public static class Program await new MissionCatalogImporter().ImportAsync(context, opts.SeedDir); await new AchievementCatalogImporter().ImportAsync(context, opts.SeedDir); await new BattlePassMonthlyMissionImporter().ImportAsync(context, opts.SeedDir); - await new DailyLoginBonusImporter().ImportAsync(context, opts.SeedDir); await new PreReleaseInfoImporter().ImportAsync(context, opts.SeedDir); await new CardListsImporter().ImportAsync(context, opts.SeedDir); await new RotationFlagUpdater().UpdateAsync(context); diff --git a/SVSim.Database/Models/DailyLoginBonusEntry.cs b/SVSim.Database/Models/DailyLoginBonusEntry.cs deleted file mode 100644 index 22dab428..00000000 --- a/SVSim.Database/Models/DailyLoginBonusEntry.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; -using SVSim.Database.Common; - -namespace SVSim.Database.Models; - -/// -/// Daily login bonus campaign from /load/index data.daily_login_bonus (dict keyed by bonus_id, -/// values are arrays of bonus days). Prod observed keys {1, 3, 4} with empty arrays — recapture -/// target during active login bonus events. -/// -public class DailyLoginBonusEntry : BaseEntity -{ - public int BonusId { get => Id; set => Id = value; } - - [Column(TypeName = "jsonb")] - public string BonusData { get; set; } = "[]"; -} diff --git a/SVSim.Database/Repositories/Globals/GlobalsRepository.cs b/SVSim.Database/Repositories/Globals/GlobalsRepository.cs index 38082e2b..2f3c1375 100644 --- a/SVSim.Database/Repositories/Globals/GlobalsRepository.cs +++ b/SVSim.Database/Repositories/Globals/GlobalsRepository.cs @@ -59,9 +59,6 @@ public class GlobalsRepository : IGlobalsRepository public Task> GetBattlePassLevels() => _dbContext.BattlePassLevels.AsNoTracking().ToListAsync(); - public Task> GetDailyLoginBonus() => - _dbContext.DailyLoginBonuses.AsNoTracking().ToListAsync(); - public Task> GetBanners() => _dbContext.Banners.AsNoTracking().OrderBy(b => b.Id).ToListAsync(); diff --git a/SVSim.Database/Repositories/Globals/IGlobalsRepository.cs b/SVSim.Database/Repositories/Globals/IGlobalsRepository.cs index 503fb56b..fe14fda7 100644 --- a/SVSim.Database/Repositories/Globals/IGlobalsRepository.cs +++ b/SVSim.Database/Repositories/Globals/IGlobalsRepository.cs @@ -19,7 +19,6 @@ public interface IGlobalsRepository Task> GetUnlimitedRestrictions(); Task> GetLoadingExclusionCards(); Task> GetBattlePassLevels(); - Task> GetDailyLoginBonus(); Task> GetBanners(); Task> GetActiveHomeDialogsAsync(DateTime nowUtc); Task GetCurrentSealedSeason(); diff --git a/SVSim.Database/SVSimDbContext.cs b/SVSim.Database/SVSimDbContext.cs index a6bb4472..eb23443e 100644 --- a/SVSim.Database/SVSimDbContext.cs +++ b/SVSim.Database/SVSimDbContext.cs @@ -60,7 +60,6 @@ public class SVSimDbContext : DbContext public DbSet ViewerMissions => Set(); public DbSet ViewerAchievements => Set(); public DbSet ViewerEventCounters => Set(); - public DbSet DailyLoginBonuses => Set(); public DbSet Banners => Set(); public DbSet HomeDialogEntries => Set(); public DbSet SealedSeasons => Set(); diff --git a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs index 9f1a198c..1701efba 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs @@ -229,10 +229,8 @@ public class LoadController : SVSimController MaintenanceCards = (await _globalsRepository.GetMaintenanceCards()) .Select(e => e.Id).ToList(), RedEtherOverrides = new List(), - // Optional per spec (load-index.md:247). Skeleton-seeded rows in DailyLoginBonuses table - // capture prod's empty-period shape ({"1":[], "3":[], "4":[]}); the spec-shaped DTO - // ({normal?, total?, campaign?[]}) carries nothing meaningful until an active campaign - // is captured. + // Optional per spec (load-index.md:247). Populated by ILoginBonusService (Task 6) + // when the viewer has an active bonus period. DailyLoginBonus = null, UserRankedMatches = new List(), UserRankInfo = RankFormats.Select(f => new UserRankInfo diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs index e7db9c4e..29eb205b 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs @@ -150,9 +150,7 @@ public class IndexResponse /// /// Spec: optional. Shape is {normal?, total?, campaign?[]} per common/types.ts.md DailyLoginBonus. - /// Until we have an active login-bonus campaign to surface in spec shape, omit. The skeleton - /// rows in DailyLoginBonuses table (prod sent {"1":[], "3":[], "4":[]}) preserve the capture - /// for archive but don't make it into the wire response. + /// Populated by ILoginBonusService when the viewer has an active bonus period; null otherwise. /// [JsonPropertyName("daily_login_bonus")] [Key("daily_login_bonus")] diff --git a/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs b/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs index f54387fc..f1fb9cb0 100644 --- a/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs +++ b/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs @@ -8,7 +8,7 @@ namespace SVSim.UnitTests.Importers; /// /// Happy-path coverage for the load-index importer classes introduced in Stage 9B -/// (RotationConfig, MyRotation, AvatarAbility, ArenaSeason, DailyLoginBonus, +/// (RotationConfig, MyRotation, AvatarAbility, ArenaSeason, /// PreReleaseInfo). Each test instantiates the importer in isolation and verifies it inserts /// rows from the corresponding seed file under Data/seeds/. /// Idempotency, edge cases, and per-importer detail tests live in dedicated *ImporterTests files (e.g. BattlePassImporterTests). @@ -73,19 +73,6 @@ public class LoadIndexImporterTests Assert.That(row!.FormatInfo, Is.Not.EqualTo("{}"), "format_info blob must be populated from seed"); } - [Test] - public async Task DailyLoginBonusImporter_writes_bonus_rows() - { - using var factory = new SVSimTestFactory(); - using var scope = factory.Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - - await new DailyLoginBonusImporter().ImportAsync(db, SeedDir); - - Assert.That(await db.DailyLoginBonuses.CountAsync(), Is.GreaterThan(0), - "daily-login-bonus.json must produce rows"); - } - [Test] public async Task PreReleaseInfoImporter_writes_singleton() { diff --git a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs index c7efe03a..60285309 100644 --- a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs +++ b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs @@ -266,7 +266,6 @@ internal class SVSimTestFactory : WebApplicationFactory await new BattlePassImporter().ImportAsync(ctx, seedDir); await new BattlePassSeasonImporter().ImportAsync(ctx, seedDir); await new BattlePassRewardImporter().ImportAsync(ctx, seedDir); - await new DailyLoginBonusImporter().ImportAsync(ctx, seedDir); await new PreReleaseInfoImporter().ImportAsync(ctx, seedDir); await new CardListsImporter().ImportAsync(ctx, seedDir); await new RotationFlagUpdater().UpdateAsync(ctx); diff --git a/SVSim.UnitTests/Repositories/GlobalsRepositoryTests.cs b/SVSim.UnitTests/Repositories/GlobalsRepositoryTests.cs index 388e97d0..a59f05db 100644 --- a/SVSim.UnitTests/Repositories/GlobalsRepositoryTests.cs +++ b/SVSim.UnitTests/Repositories/GlobalsRepositoryTests.cs @@ -80,16 +80,6 @@ public class GlobalsRepositoryTests Assert.That(levels.Count, Is.EqualTo(100)); } - [Test] - public async Task GetDailyLoginBonus_returns_three_skeleton_entries() - { - var (factory, repo) = await SetupAsync(); - using var _ = factory; - var bonuses = await repo.GetDailyLoginBonus(); - Assert.That(bonuses.Count, Is.EqualTo(3), - "Prod capture has keys {1, 3, 4} with empty arrays — skeleton presence still seeded."); - } - [Test] public async Task GetPreReleaseInfo_returns_singleton() { From 107ee106a368fbce631c93b879653a1d2a3b88d8 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 15:42:40 -0400 Subject: [PATCH 2/9] feat(viewer): add LastLoginBonusClaimedAt + LoginBonusStreak columns Drops the unused daily_login_bonuses table in the same migration. Co-Authored-By: Claude Sonnet 4.6 --- ...93716_AddViewerLoginBonusState.Designer.cs | 4778 +++++++++++++++++ ...20260613193716_AddViewerLoginBonusState.cs | 58 + .../Migrations/SVSimDbContextModelSnapshot.cs | 29 +- SVSim.Database/Models/Viewer.cs | 12 + 4 files changed, 4854 insertions(+), 23 deletions(-) create mode 100644 SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.Designer.cs create mode 100644 SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.cs diff --git a/SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.Designer.cs b/SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.Designer.cs new file mode 100644 index 00000000..fca60dde --- /dev/null +++ b/SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.Designer.cs @@ -0,0 +1,4778 @@ +// +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("20260613193716_AddViewerLoginBonusState")] + partial class AddViewerLoginBonusState + { + /// + 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.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("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("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.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.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("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.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/20260613193716_AddViewerLoginBonusState.cs b/SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.cs new file mode 100644 index 00000000..4534c8db --- /dev/null +++ b/SVSim.Database/Migrations/20260613193716_AddViewerLoginBonusState.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + /// + public partial class AddViewerLoginBonusState : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DailyLoginBonuses"); + + migrationBuilder.AddColumn( + name: "LastLoginBonusClaimedAt", + table: "Viewers", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LoginBonusStreak", + table: "Viewers", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastLoginBonusClaimedAt", + table: "Viewers"); + + migrationBuilder.DropColumn( + name: "LoginBonusStreak", + table: "Viewers"); + + migrationBuilder.CreateTable( + name: "DailyLoginBonuses", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false), + BonusData = table.Column(type: "jsonb", nullable: false), + BonusId = table.Column(type: "integer", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DailyLoginBonuses", x => x.Id); + }); + } + } +} diff --git a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs index 992a1109..220de977 100644 --- a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs +++ b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs @@ -1075,29 +1075,6 @@ namespace SVSim.Database.Migrations b.ToTable("ColosseumWindFallDecks"); }); - modelBuilder.Entity("SVSim.Database.Models.DailyLoginBonusEntry", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("BonusData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("BonusId") - .HasColumnType("integer"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateUpdated") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("DailyLoginBonuses"); - }); - modelBuilder.Entity("SVSim.Database.Models.DefaultDeckEntry", b => { b.Property("Id") @@ -2711,6 +2688,12 @@ namespace SVSim.Database.Migrations 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"); diff --git a/SVSim.Database/Models/Viewer.cs b/SVSim.Database/Models/Viewer.cs index 59861dbd..4971bf21 100644 --- a/SVSim.Database/Models/Viewer.cs +++ b/SVSim.Database/Models/Viewer.cs @@ -33,6 +33,18 @@ public class Viewer : BaseEntity public DateTime LastLogin { get; set; } + /// + /// UTC instant of the most recent daily-login-bonus claim. Null = never claimed. + /// Compared against JST 02:00 day boundary via JstPeriod.DayKey to decide eligibility. + /// + public DateTime? LastLoginBonusClaimedAt { get; set; } + + /// + /// 1-based position in the Normal login-bonus cycle. 0 = never claimed (first claim + /// will write 1). Wraps back to 1 after reaching cycle length (LoginBonusConfig.Normal.Count). + /// + public int LoginBonusStreak { get; set; } + /// BGType enum: 0=Deck, 1=CustomBG, 2=RandomBG. Default 0 = follow equipped deck's leader skin. public int MyPageBgSelectType { get; set; } From 6fb73c7a0991ea26256bcd099b86639bc31a3dce Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 15:50:42 -0400 Subject: [PATCH 3/9] feat(config): LoginBonus section with 15-day shipped normal cycle Rewards copied verbatim from traffic_prod_tutorial.ndjson capture. --- .../Models/Config/LoginBonusConfig.cs | 58 +++++++++++++++++++ .../Config/LoginBonusConfigTests.cs | 37 ++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 SVSim.Database/Models/Config/LoginBonusConfig.cs create mode 100644 SVSim.UnitTests/Config/LoginBonusConfigTests.cs diff --git a/SVSim.Database/Models/Config/LoginBonusConfig.cs b/SVSim.Database/Models/Config/LoginBonusConfig.cs new file mode 100644 index 00000000..93a17ea9 --- /dev/null +++ b/SVSim.Database/Models/Config/LoginBonusConfig.cs @@ -0,0 +1,58 @@ +using SVSim.Database.Enums; + +namespace SVSim.Database.Models.Config; + +/// +/// Daily login bonus catalog. The Normal cycle is the always-on N-day streak. Total +/// (continuous milestones) and Campaign (date-bounded specials) are placeholders — wire +/// emits empty arrays for them until a campaign is captured and seeded here. +/// +[ConfigSection("LoginBonus")] +public class LoginBonusConfig +{ + public int CampaignId { get; set; } = 3; + public string Name { get; set; } = "Daily Bonus"; + + /// BG image asset key — wire ships as string. Prod captured value is "0". + public string Img { get; set; } = "0"; + + /// + /// Normal-cycle reward entries, 1-based by Day. Defaults to a 15-day cycle copied + /// verbatim from the prod tutorial capture (traffic_prod_tutorial.ndjson). + /// + public List Normal { get; set; } = new(); + + public static LoginBonusConfig ShippedDefaults() => new() + { + CampaignId = 3, + Name = "Daily Bonus", + Img = "0", + Normal = new List + { + new() { Day = 1, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 20 }, + new() { Day = 2, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 20 }, + new() { Day = 3, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 20 }, + new() { Day = 4, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 20 }, + new() { Day = 5, EffectId = 2, RewardType = UserGoodsType.Item, RewardDetailId = 80001, RewardNumber = 1 }, + new() { Day = 6, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 30 }, + new() { Day = 7, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 30 }, + new() { Day = 8, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 30 }, + new() { Day = 9, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 30 }, + new() { Day = 10, EffectId = 2, RewardType = UserGoodsType.Item, RewardDetailId = 80001, RewardNumber = 1 }, + new() { Day = 11, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 40 }, + new() { Day = 12, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 40 }, + new() { Day = 13, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 40 }, + new() { Day = 14, EffectId = 1, RewardType = UserGoodsType.Rupy, RewardDetailId = 0, RewardNumber = 40 }, + new() { Day = 15, EffectId = 2, RewardType = UserGoodsType.Item, RewardDetailId = 1, RewardNumber = 1 }, + }, + }; +} + +public class NormalBonusEntry +{ + public int Day { get; set; } + public int EffectId { get; set; } = 1; + public UserGoodsType RewardType { get; set; } + public long RewardDetailId { get; set; } + public int RewardNumber { get; set; } +} diff --git a/SVSim.UnitTests/Config/LoginBonusConfigTests.cs b/SVSim.UnitTests/Config/LoginBonusConfigTests.cs new file mode 100644 index 00000000..d900e20a --- /dev/null +++ b/SVSim.UnitTests/Config/LoginBonusConfigTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using SVSim.Database.Enums; +using SVSim.Database.Models.Config; + +namespace SVSim.UnitTests.Config; + +public class LoginBonusConfigTests +{ + [Test] + public void ShippedDefaults_matches_prod_normal_cycle() + { + var cfg = LoginBonusConfig.ShippedDefaults(); + + Assert.That(cfg.CampaignId, Is.EqualTo(3)); + Assert.That(cfg.Name, Is.EqualTo("Daily Bonus")); + Assert.That(cfg.Img, Is.EqualTo("0")); + Assert.That(cfg.Normal, Has.Count.EqualTo(15)); + + // Day 1: 20 Rupy + Assert.That(cfg.Normal[0].Day, Is.EqualTo(1)); + Assert.That(cfg.Normal[0].RewardType, Is.EqualTo(UserGoodsType.Rupy)); + Assert.That(cfg.Normal[0].RewardDetailId, Is.EqualTo(0)); + Assert.That(cfg.Normal[0].RewardNumber, Is.EqualTo(20)); + Assert.That(cfg.Normal[0].EffectId, Is.EqualTo(1)); + + // Day 5: Item 80001 ×1 with effect 2 (the highlight ticket) + Assert.That(cfg.Normal[4].RewardType, Is.EqualTo(UserGoodsType.Item)); + Assert.That(cfg.Normal[4].RewardDetailId, Is.EqualTo(80001)); + Assert.That(cfg.Normal[4].RewardNumber, Is.EqualTo(1)); + Assert.That(cfg.Normal[4].EffectId, Is.EqualTo(2)); + + // Day 15: Item id 1 ×1 with effect 2 (cycle capstone) + Assert.That(cfg.Normal[14].RewardType, Is.EqualTo(UserGoodsType.Item)); + Assert.That(cfg.Normal[14].RewardDetailId, Is.EqualTo(1)); + Assert.That(cfg.Normal[14].RewardNumber, Is.EqualTo(1)); + } +} From 0d01727e480bc6cb6755a63e72d121aa5b37d678 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 15:55:12 -0400 Subject: [PATCH 4/9] chore(config): split NormalBonusEntry, strengthen Normal-defaults warning File-per-class convention + explicit warning against the property-initializer collection-default bug class (2026-05-24). --- SVSim.Database/Models/Config/LoginBonusConfig.cs | 16 +++++----------- SVSim.Database/Models/Config/NormalBonusEntry.cs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 SVSim.Database/Models/Config/NormalBonusEntry.cs diff --git a/SVSim.Database/Models/Config/LoginBonusConfig.cs b/SVSim.Database/Models/Config/LoginBonusConfig.cs index 93a17ea9..f8109ee9 100644 --- a/SVSim.Database/Models/Config/LoginBonusConfig.cs +++ b/SVSim.Database/Models/Config/LoginBonusConfig.cs @@ -17,8 +17,11 @@ public class LoginBonusConfig public string Img { get; set; } = "0"; /// - /// Normal-cycle reward entries, 1-based by Day. Defaults to a 15-day cycle copied - /// verbatim from the prod tutorial capture (traffic_prod_tutorial.ndjson). + /// Normal-cycle reward entries, 1-based by Day. Defaults to new() (empty); + /// real entries live in . Do not move them into this + /// property initializer — collection initializers silently empty out under + /// GameConfigService.Get<T>()'s tier-merge (config-defaults convention, + /// historical bug 2026-05-24). /// public List Normal { get; set; } = new(); @@ -47,12 +50,3 @@ public class LoginBonusConfig }, }; } - -public class NormalBonusEntry -{ - public int Day { get; set; } - public int EffectId { get; set; } = 1; - public UserGoodsType RewardType { get; set; } - public long RewardDetailId { get; set; } - public int RewardNumber { get; set; } -} diff --git a/SVSim.Database/Models/Config/NormalBonusEntry.cs b/SVSim.Database/Models/Config/NormalBonusEntry.cs new file mode 100644 index 00000000..8f78ee76 --- /dev/null +++ b/SVSim.Database/Models/Config/NormalBonusEntry.cs @@ -0,0 +1,12 @@ +using SVSim.Database.Enums; + +namespace SVSim.Database.Models.Config; + +public class NormalBonusEntry +{ + public int Day { get; set; } + public int EffectId { get; set; } = 1; + public UserGoodsType RewardType { get; set; } + public long RewardDetailId { get; set; } + public int RewardNumber { get; set; } +} From e6e3f919a675dc52494e68869abd241b87e00b84 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 15:59:42 -0400 Subject: [PATCH 5/9] fix(dto): align daily_login_bonus DTOs with prod wire shape - numeric fields ship as quoted strings (campaign_id, img, effect_id, reward_type, reward_detail_id, reward_number) - DailyLoginBonus.Campaign is a List<>, not a single object - LoginBonusCampaign.Img replaces Image (string, asset-key) - add IsOneDayMultiRewards (SpecialData-only on client, byte-faithful) --- .../Models/Dtos/DailyLoginBonus.cs | 26 +++++--- .../Models/Dtos/LoginBonusCampaign.cs | 45 +++++++++----- .../Models/Dtos/LoginBonusReward.cs | 45 ++++++-------- .../Models/Dtos/Responses/IndexResponse.cs | 6 +- .../Dtos/DailyLoginBonusWireShapeTests.cs | 59 +++++++++++++++++++ 5 files changed, 126 insertions(+), 55 deletions(-) create mode 100644 SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs index b72e0eb4..5346c40b 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs @@ -3,16 +3,24 @@ using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos; +/// +/// Wire shape: { normal?, total?, campaign? (array) }. Client parser at +/// LoadDetail.cs:553 reads exactly these three keys. normal presence drives +/// IsExistLoginBonusData() / popup eligibility. +/// [MessagePackObject] public class DailyLoginBonus { - [JsonPropertyName("total")] - [Key("total")] - public LoginBonusCampaign? Total { get; set; } - [JsonPropertyName("normal")] - [Key("normal")] + [JsonPropertyName("normal")] [Key("normal")] public LoginBonusCampaign? Normal { get; set; } - [JsonPropertyName("campaign")] - [Key("campaign")] - public LoginBonusCampaign? Campaign { get; set; } -} \ No newline at end of file + + [JsonPropertyName("total")] [Key("total")] + public LoginBonusCampaign? Total { get; set; } + + /// + /// Array of campaign panels (LoadDetail.cs:583 iterates jsonData3[i]). Empty list + /// when no specials are active — prod sends [] not null in that case. + /// + [JsonPropertyName("campaign")] [Key("campaign")] + public List Campaign { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs index 7a4aedeb..65eab5b7 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs @@ -3,25 +3,38 @@ using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos; +/// +/// One login-bonus campaign panel. Used for normal, each campaign array entry, and (when +/// populated) total. campaign_id and img ship as strings on the wire. Client +/// consumers: NormalData (reads name, now_count, is_next_reward, reward[]), +/// SpecialData (also reads start_date, end_date, img, is_one_day_multi_rewards), +/// ContinuousData (reads only reward[]). +/// [MessagePackObject] public class LoginBonusCampaign { - [JsonPropertyName("name")] - [Key("name")] + [JsonPropertyName("name")] [Key("name")] public string Name { get; set; } = string.Empty; - [JsonPropertyName("campaign_id")] - [Key("campaign_id")] - public int CampaignId { get; set; } - [JsonPropertyName("img")] - [Key("img")] - public int Image { get; set; } - [JsonPropertyName("now_count")] - [Key("now_count")] + + [JsonPropertyName("campaign_id")] [Key("campaign_id")] + public string CampaignId { get; set; } = "0"; + + [JsonPropertyName("img")] [Key("img")] + public string Img { get; set; } = "0"; + + [JsonPropertyName("now_count")] [Key("now_count")] public int NowCount { get; set; } - [JsonPropertyName("is_next_reward")] - [Key("is_next_reward")] + + [JsonPropertyName("is_next_reward")] [Key("is_next_reward")] public bool IsNextReward { get; set; } - [JsonPropertyName("reward")] - [Key("reward")] - public List Rewards { get; set; } = new List(); -} \ No newline at end of file + + [JsonPropertyName("reward")] [Key("reward")] + public List Rewards { get; set; } = new(); + + /// + /// SpecialData-only on the client (SpecialData.cs:50). Server emits it on every panel + /// for byte-faithful replay; client ignores it on Normal/Total paths. + /// + [JsonPropertyName("is_one_day_multi_rewards")] [Key("is_one_day_multi_rewards")] + public bool IsOneDayMultiRewards { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs index 5e3c04da..28cd4788 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs @@ -3,34 +3,23 @@ using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos; +/// +/// One day's reward in a login-bonus cycle. All numeric fields ship as quoted strings on +/// the wire — prod-confirmed shape from traffic_prod_tutorial.ndjson. Client uses +/// JsonData.ToInt() so it tolerates either form, but we match prod exactly. +/// [MessagePackObject] public class LoginBonusReward { - /// - /// The effect shown ingame. - /// - [JsonPropertyName("effect_id")] - [Key("effect_id")] - public int EffectId { get; set; } - - /// - /// The type of reward. - /// - [JsonPropertyName("reward_type")] - [Key("reward_type")] - public int RewardType { get; set; } - - /// - /// A more specified reward type (ie if a pack, which pack). - /// - [JsonPropertyName("reward_detail_id")] - [Key("reward_detail_id")] - public int RewardDetailId { get; set; } - - /// - /// The count of the reward. - /// - [JsonPropertyName("reward_number")] - [Key("reward_number")] - public int RewardNumber { get; set; } -} \ No newline at end of file + [JsonPropertyName("effect_id")] [Key("effect_id")] + public string EffectId { get; set; } = "0"; + + [JsonPropertyName("reward_type")] [Key("reward_type")] + public string RewardType { get; set; } = "0"; + + [JsonPropertyName("reward_detail_id")] [Key("reward_detail_id")] + public string RewardDetailId { get; set; } = "0"; + + [JsonPropertyName("reward_number")] [Key("reward_number")] + public string RewardNumber { get; set; } = "0"; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs index 29eb205b..85aee91f 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs @@ -149,8 +149,10 @@ public class IndexResponse public List UserRankedMatches { get; set; } = new(); /// - /// Spec: optional. Shape is {normal?, total?, campaign?[]} per common/types.ts.md DailyLoginBonus. - /// Populated by ILoginBonusService when the viewer has an active bonus period; null otherwise. + /// Spec: optional. Shape {normal?, total?, campaign?[]} — client parses three keys + /// in LoadDetail.cs:553. normal presence triggers the login-bonus popup + /// (NextSceneSwitcher.cs:103). Populated by ILoginBonusService at /load/index time; + /// null when the viewer has already claimed today's bonus. /// [JsonPropertyName("daily_login_bonus")] [Key("daily_login_bonus")] diff --git a/SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs b/SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs new file mode 100644 index 00000000..8513a6e4 --- /dev/null +++ b/SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using NUnit.Framework; +using SVSim.EmulatedEntrypoint.Models.Dtos; + +namespace SVSim.UnitTests.Dtos; + +public class DailyLoginBonusWireShapeTests +{ + private static readonly JsonSerializerOptions Opts = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; + + [Test] + public void Reward_numeric_fields_serialize_as_strings() + { + var r = new LoginBonusReward + { + EffectId = "1", RewardType = "9", RewardDetailId = "0", RewardNumber = "20" + }; + + var json = JsonSerializer.Serialize(r, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.GetProperty("effect_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("reward_type").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("reward_detail_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("reward_number").ValueKind, Is.EqualTo(JsonValueKind.String)); + } + + [Test] + public void Campaign_id_and_img_serialize_as_strings() + { + var c = new LoginBonusCampaign { CampaignId = "3", Img = "0", Name = "Daily Bonus" }; + var json = JsonSerializer.Serialize(c, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.GetProperty("campaign_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("img").ValueKind, Is.EqualTo(JsonValueKind.String)); + } + + [Test] + public void Campaign_supports_is_one_day_multi_rewards_flag() + { + var c = new LoginBonusCampaign { IsOneDayMultiRewards = false }; + var json = JsonSerializer.Serialize(c, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.TryGetProperty("is_one_day_multi_rewards", out var v), Is.True); + Assert.That(v.ValueKind, Is.EqualTo(JsonValueKind.False)); + } + + [Test] + public void DailyLoginBonus_campaign_is_an_array() + { + var d = new DailyLoginBonus { Normal = new LoginBonusCampaign(), Campaign = new List() }; + var json = JsonSerializer.Serialize(d, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.GetProperty("campaign").ValueKind, Is.EqualTo(JsonValueKind.Array)); + } +} From b831810fc1f4460d6a4fccc4efec767cb32fd2c1 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 16:04:07 -0400 Subject: [PATCH 6/9] =?UTF-8?q?chore(dto):=20correct=20LoginBonusCampaign?= =?UTF-8?q?=20doc=20=E2=80=94=20start/end=5Fdate=20not=20modeled=20yet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs index 65eab5b7..b1cb0d95 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs @@ -7,7 +7,8 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos; /// One login-bonus campaign panel. Used for normal, each campaign array entry, and (when /// populated) total. campaign_id and img ship as strings on the wire. Client /// consumers: NormalData (reads name, now_count, is_next_reward, reward[]), -/// SpecialData (also reads start_date, end_date, img, is_one_day_multi_rewards), +/// SpecialData (also reads img, is_one_day_multi_rewards — start_date/end_date are not +/// modeled yet and ship absent until a real campaign capture lands), /// ContinuousData (reads only reward[]). /// [MessagePackObject] From 5083d43e51313e33e20205ab1a98ea9c8f9489de Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 16:15:21 -0400 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20ILoginBonusService=20=E2=80=94=20JS?= =?UTF-8?q?T=20day-bucketed=20claim=20+=20grant=20+=20DTO=20assembly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GrantIfDueAsync advances streak (1..15 cycle), grants the day's reward via inventory tx, returns wire-shape DailyLoginBonus. IsDue helper for MyPage. Also fixes GameConfigurationJsonbTests section-count (15→16) which was broken since Task 3 added the LoginBonus [ConfigSection] but the assertion wasn't updated. --- SVSim.EmulatedEntrypoint/Program.cs | 1 + .../Services/ILoginBonusService.cs | 22 ++ .../Services/LoginBonusService.cs | 68 +++++++ .../Models/GameConfigurationJsonbTests.cs | 6 +- .../Services/LoginBonusServiceTests.cs | 190 ++++++++++++++++++ 5 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 SVSim.EmulatedEntrypoint/Services/ILoginBonusService.cs create mode 100644 SVSim.EmulatedEntrypoint/Services/LoginBonusService.cs create mode 100644 SVSim.UnitTests/Services/LoginBonusServiceTests.cs diff --git a/SVSim.EmulatedEntrypoint/Program.cs b/SVSim.EmulatedEntrypoint/Program.cs index 16a7ab6b..4c635f8b 100644 --- a/SVSim.EmulatedEntrypoint/Program.cs +++ b/SVSim.EmulatedEntrypoint/Program.cs @@ -113,6 +113,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/SVSim.EmulatedEntrypoint/Services/ILoginBonusService.cs b/SVSim.EmulatedEntrypoint/Services/ILoginBonusService.cs new file mode 100644 index 00000000..4d83a4e8 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Services/ILoginBonusService.cs @@ -0,0 +1,22 @@ +using SVSim.Database.Services.Inventory; +using SVSim.EmulatedEntrypoint.Models.Dtos; + +namespace SVSim.EmulatedEntrypoint.Services; + +/// +/// Daily login bonus controller-facing API. Single entry point: called from +/// /load/index inside the existing inventory transaction. Returns null when the viewer +/// has already claimed today's bonus. +/// +public interface ILoginBonusService +{ + /// + /// If due, advances tx.Viewer.LoginBonusStreak + LastLoginBonusClaimedAt, + /// grants the day's reward via , and returns the populated wire + /// DTO. If not due, returns null and leaves viewer state untouched. + /// + Task GrantIfDueAsync(IInventoryTransaction tx, CancellationToken ct = default); + + /// Read-only "would GrantIfDueAsync emit a bonus right now?" — for MyPage flag. + bool IsDue(SVSim.Database.Models.Viewer viewer); +} diff --git a/SVSim.EmulatedEntrypoint/Services/LoginBonusService.cs b/SVSim.EmulatedEntrypoint/Services/LoginBonusService.cs new file mode 100644 index 00000000..2c785d42 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Services/LoginBonusService.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using SVSim.Database.Models; +using SVSim.Database.Models.Config; +using SVSim.Database.Services; +using SVSim.Database.Services.Inventory; +using SVSim.EmulatedEntrypoint.Models.Dtos; + +namespace SVSim.EmulatedEntrypoint.Services; + +public class LoginBonusService : ILoginBonusService +{ + private readonly IGameConfigService _config; + private readonly TimeProvider _time; + + public LoginBonusService(IGameConfigService config, TimeProvider time) + { + _config = config; + _time = time; + } + + public bool IsDue(Viewer viewer) + { + if (viewer.LastLoginBonusClaimedAt is null) return true; + var now = _time.GetUtcNow(); + var lastClaim = new DateTimeOffset( + DateTime.SpecifyKind(viewer.LastLoginBonusClaimedAt.Value, DateTimeKind.Utc)); + return JstPeriod.DayKey(lastClaim) != JstPeriod.DayKey(now); + } + + public async Task GrantIfDueAsync(IInventoryTransaction tx, CancellationToken ct = default) + { + var viewer = tx.Viewer; + if (!IsDue(viewer)) return null; + + var cfg = _config.Get(); + if (cfg.Normal.Count == 0) return null; // catalog misconfigured — nothing to grant + + int newStreak = (viewer.LoginBonusStreak % cfg.Normal.Count) + 1; + var entry = cfg.Normal[newStreak - 1]; + + await tx.GrantAsync(entry.RewardType, entry.RewardDetailId, entry.RewardNumber, ct); + + viewer.LoginBonusStreak = newStreak; + viewer.LastLoginBonusClaimedAt = _time.GetUtcNow().UtcDateTime; + + return new DailyLoginBonus + { + Normal = new LoginBonusCampaign + { + Name = cfg.Name, + CampaignId = cfg.CampaignId.ToString(CultureInfo.InvariantCulture), + Img = cfg.Img, + NowCount = newStreak, + IsNextReward = true, + IsOneDayMultiRewards = false, + Rewards = cfg.Normal.Select(n => new LoginBonusReward + { + EffectId = n.EffectId.ToString(CultureInfo.InvariantCulture), + RewardType = ((int)n.RewardType).ToString(CultureInfo.InvariantCulture), + RewardDetailId = n.RewardDetailId.ToString(CultureInfo.InvariantCulture), + RewardNumber = n.RewardNumber.ToString(CultureInfo.InvariantCulture), + }).ToList(), + }, + Total = null, + Campaign = new List(), + }; + } +} diff --git a/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs b/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs index fe9d7ee5..506ab09e 100644 --- a/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs +++ b/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs @@ -25,14 +25,14 @@ public class GameConfigurationJsonbTests var rows = await db.GameConfigs.AsNoTracking().ToListAsync(); var byName = rows.ToDictionary(r => r.SectionName); - // One row per [ConfigSection]-marked POCO (15 sections today: Player, DefaultGrants, + // One row per [ConfigSection]-marked POCO (16 sections today: Player, DefaultGrants, // DefaultLoadout, Challenge, Rotation, PackRates, MyRotationSchedule, Story, ResourceConfig, - // Freeplay, ArenaTwoPick, Matching, CardMasterConfig, ColosseumSeason, ColosseumRounds). + // Freeplay, ArenaTwoPick, Matching, CardMasterConfig, ColosseumSeason, ColosseumRounds, LoginBonus). Assert.That(byName.Keys, Is.EquivalentTo(new[] { "Player", "DefaultGrants", "DefaultLoadout", "Challenge", "Rotation", "PackRates", "MyRotationSchedule", "Story", "ResourceConfig", "Freeplay", "ArenaTwoPick", "Matching", - "CardMasterConfig", "ColosseumSeason", "ColosseumRounds", + "CardMasterConfig", "ColosseumSeason", "ColosseumRounds", "LoginBonus", })); var resources = JsonSerializer.Deserialize(byName["ResourceConfig"].ValueJson)!; diff --git a/SVSim.UnitTests/Services/LoginBonusServiceTests.cs b/SVSim.UnitTests/Services/LoginBonusServiceTests.cs new file mode 100644 index 00000000..ac007f81 --- /dev/null +++ b/SVSim.UnitTests/Services/LoginBonusServiceTests.cs @@ -0,0 +1,190 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Services.Inventory; +using SVSim.EmulatedEntrypoint.Services; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Services; + +public class LoginBonusServiceTests +{ + private static async Task<(SVSimTestFactory factory, long viewerId)> SetupAsync() + { + var factory = new SVSimTestFactory(); + var viewerId = await factory.SeedViewerAsync(); + return (factory, viewerId); + } + + private static async Task ReloadViewerAsync(SVSimTestFactory f, long viewerId) + { + using var scope = f.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await db.Viewers.Include(v => v.Currency).FirstAsync(v => v.Id == viewerId); + } + + [Test] + public async Task FirstClaim_returns_dto_with_now_count_1() + { + var (factory, viewerId) = await SetupAsync(); + using var _ = factory; + using var scope = factory.Services.CreateScope(); + var inv = scope.ServiceProvider.GetRequiredService(); + var bonus = scope.ServiceProvider.GetRequiredService(); + + await using var tx = await inv.BeginAsync(viewerId); + var dto = await bonus.GrantIfDueAsync(tx); + await tx.CommitAsync(); + + Assert.That(dto, Is.Not.Null); + Assert.That(dto!.Normal, Is.Not.Null); + Assert.That(dto.Normal!.NowCount, Is.EqualTo(1)); + Assert.That(dto.Normal.Rewards, Has.Count.EqualTo(15)); + Assert.That(dto.Total, Is.Null); + Assert.That(dto.Campaign, Is.Empty); + } + + [Test] + public async Task SecondClaim_same_day_returns_null() + { + var (factory, viewerId) = await SetupAsync(); + using var _ = factory; + + using (var scope1 = factory.Services.CreateScope()) + { + var inv1 = scope1.ServiceProvider.GetRequiredService(); + var bonus1 = scope1.ServiceProvider.GetRequiredService(); + await using var tx1 = await inv1.BeginAsync(viewerId); + await bonus1.GrantIfDueAsync(tx1); + await tx1.CommitAsync(); + } + + using var scope2 = factory.Services.CreateScope(); + var inv2 = scope2.ServiceProvider.GetRequiredService(); + var bonus2 = scope2.ServiceProvider.GetRequiredService(); + await using var tx2 = await inv2.BeginAsync(viewerId); + var dto = await bonus2.GrantIfDueAsync(tx2); + + Assert.That(dto, Is.Null); + } + + [Test] + public async Task Claim_after_rollover_advances_streak() + { + var (factory, viewerId) = await SetupAsync(); + using var _ = factory; + + using (var s1 = factory.Services.CreateScope()) + { + var inv = s1.ServiceProvider.GetRequiredService(); + var bonus = s1.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + await bonus.GrantIfDueAsync(tx); + await tx.CommitAsync(); + } + + // Backdate the claim timestamp by 2 days to simulate a day rollover + using (var s = factory.Services.CreateScope()) + { + var db = s.ServiceProvider.GetRequiredService(); + var v = await db.Viewers.FirstAsync(v => v.Id == viewerId); + v.LastLoginBonusClaimedAt = DateTime.UtcNow.AddDays(-2); + await db.SaveChangesAsync(); + } + + using var s2 = factory.Services.CreateScope(); + var inv2 = s2.ServiceProvider.GetRequiredService(); + var bonus2 = s2.ServiceProvider.GetRequiredService(); + await using var tx2 = await inv2.BeginAsync(viewerId); + var dto = await bonus2.GrantIfDueAsync(tx2); + await tx2.CommitAsync(); + + Assert.That(dto, Is.Not.Null); + Assert.That(dto!.Normal!.NowCount, Is.EqualTo(2)); + var v2 = await ReloadViewerAsync(factory, viewerId); + Assert.That(v2.LoginBonusStreak, Is.EqualTo(2)); + } + + [Test] + public async Task Streak_wraps_after_cycle_length() + { + var (factory, viewerId) = await SetupAsync(); + using var _ = factory; + + // Force the viewer into "just claimed day 15 yesterday" + using (var s = factory.Services.CreateScope()) + { + var db = s.ServiceProvider.GetRequiredService(); + var v = await db.Viewers.FirstAsync(v => v.Id == viewerId); + v.LoginBonusStreak = 15; + v.LastLoginBonusClaimedAt = DateTime.UtcNow.AddDays(-1); + await db.SaveChangesAsync(); + } + + using var s2 = factory.Services.CreateScope(); + var inv = s2.ServiceProvider.GetRequiredService(); + var bonus = s2.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + var dto = await bonus.GrantIfDueAsync(tx); + await tx.CommitAsync(); + + Assert.That(dto!.Normal!.NowCount, Is.EqualTo(1)); + var v2 = await ReloadViewerAsync(factory, viewerId); + Assert.That(v2.LoginBonusStreak, Is.EqualTo(1)); + } + + [Test] + public async Task Grant_actually_credits_rupy_for_day1() + { + var (factory, viewerId) = await SetupAsync(); + using var _ = factory; + + long before; + using (var s = factory.Services.CreateScope()) + { + var db = s.ServiceProvider.GetRequiredService(); + var v = await db.Viewers.Include(x => x.Currency).FirstAsync(v => v.Id == viewerId); + before = (long)v.Currency.Rupees; + } + + using var scope = factory.Services.CreateScope(); + var inv = scope.ServiceProvider.GetRequiredService(); + var bonus = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + await bonus.GrantIfDueAsync(tx); + await tx.CommitAsync(); + + using var s2 = factory.Services.CreateScope(); + var db2 = s2.ServiceProvider.GetRequiredService(); + var after = (long)(await db2.Viewers.Include(x => x.Currency).FirstAsync(v => v.Id == viewerId)).Currency.Rupees; + + Assert.That(after - before, Is.EqualTo(20), "Day 1 = 20 Rupy per catalog"); + } + + [Test] + public async Task IsDue_is_false_after_claim_same_day() + { + var (factory, viewerId) = await SetupAsync(); + using var _ = factory; + + using var scope = factory.Services.CreateScope(); + var inv = scope.ServiceProvider.GetRequiredService(); + var bonus = scope.ServiceProvider.GetRequiredService(); + + var v0 = await ReloadViewerAsync(factory, viewerId); + Assert.That(bonus.IsDue(v0), Is.True, "fresh viewer is due"); + + await using var tx = await inv.BeginAsync(viewerId); + await bonus.GrantIfDueAsync(tx); + await tx.CommitAsync(); + + var v1 = await ReloadViewerAsync(factory, viewerId); + Assert.That(bonus.IsDue(v1), Is.False); + } +} From 7d83560fc9168ec42876656f40c469b51a1b44cb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 16:40:44 -0400 Subject: [PATCH 8/9] feat(load): emit daily_login_bonus via ILoginBonusService Grant runs inside the existing inventory tx so streak-bump and reward credit commit atomically with cosmetic backfill. --- .../Controllers/LoadController.cs | 10 ++-- .../Controllers/LoadControllerTests.cs | 48 +++++++++++++++++-- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs index 1701efba..536148be 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs @@ -49,12 +49,14 @@ public class LoadController : SVSimController private readonly SVSimDbContext _db; private readonly IInventoryService _inv; private readonly ICardMasterPayloadProvider _cardMaster; + private readonly ILoginBonusService _loginBonus; public LoadController(IViewerRepository viewerRepository, IGlobalsRepository globalsRepository, IGameConfigService config, IBattlePassService battlePass, IViewerMissionStateService missionState, SVSimDbContext db, IInventoryService inv, - ICardMasterPayloadProvider cardMaster) + ICardMasterPayloadProvider cardMaster, + ILoginBonusService loginBonus) { _viewerRepository = viewerRepository; _globalsRepository = globalsRepository; @@ -64,6 +66,7 @@ public class LoadController : SVSimController _db = db; _inv = inv; _cardMaster = cardMaster; + _loginBonus = loginBonus; } [HttpPost("index")] @@ -88,6 +91,7 @@ public class LoadController : SVSimController // the response payload would be one /load/index behind on newly-granted cosmetics. await using var tx = await _inv.BeginAsync(viewer.Id, ct); await tx.BackfillCardCosmeticsAsync(ct); + DailyLoginBonus? loginBonusDto = await _loginBonus.GrantIfDueAsync(tx, ct); await tx.CommitAsync(ct); // Lazy-materialize mission/achievement state. Idempotent — safe to call every /load/index. @@ -229,9 +233,7 @@ public class LoadController : SVSimController MaintenanceCards = (await _globalsRepository.GetMaintenanceCards()) .Select(e => e.Id).ToList(), RedEtherOverrides = new List(), - // Optional per spec (load-index.md:247). Populated by ILoginBonusService (Task 6) - // when the viewer has an active bonus period. - DailyLoginBonus = null, + DailyLoginBonus = loginBonusDto, UserRankedMatches = new List(), UserRankInfo = RankFormats.Select(f => new UserRankInfo { diff --git a/SVSim.UnitTests/Controllers/LoadControllerTests.cs b/SVSim.UnitTests/Controllers/LoadControllerTests.cs index 1bef342c..acbb37cc 100644 --- a/SVSim.UnitTests/Controllers/LoadControllerTests.cs +++ b/SVSim.UnitTests/Controllers/LoadControllerTests.cs @@ -307,9 +307,11 @@ public class LoadControllerTests Assert.That(root.GetProperty("rotation_card_set_id_list").GetArrayLength(), Is.GreaterThanOrEqualTo(2)); - // Optional/absent fields stay absent when nothing meaningful to surface - Assert.That(root.TryGetProperty("daily_login_bonus", out _), Is.False, - "daily_login_bonus optional per spec; emit null when no active campaign"); + // daily_login_bonus IS emitted for a fresh viewer (LastLoginBonusClaimedAt is null → + // IsDue returns true). The seeded globals test uses a fresh viewer, so it gets Day 1. + Assert.That(root.TryGetProperty("daily_login_bonus", out var dlbGlobals), Is.True, + "daily_login_bonus should be present for a fresh viewer (IsDue=true)"); + Assert.That(dlbGlobals.ValueKind, Is.EqualTo(JsonValueKind.Object)); // battle_pass_level_info is present when levels are seeded — 100-entry dict keyed by level string. Assert.That(root.TryGetProperty("battle_pass_level_info", out var bpli), Is.True, @@ -380,6 +382,46 @@ public class LoadControllerTests } } + [Test] + public async Task Index_emits_daily_login_bonus_for_fresh_viewer() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + + var root = await PostIndexAndReadBody(factory, viewerId); + + Assert.That(root.TryGetProperty("daily_login_bonus", out var dlb), Is.True); + Assert.That(dlb.ValueKind, Is.EqualTo(JsonValueKind.Object)); + + var normal = dlb.GetProperty("normal"); + Assert.That(normal.GetProperty("now_count").GetInt32(), Is.EqualTo(1)); + Assert.That(normal.GetProperty("name").GetString(), Is.EqualTo("Daily Bonus")); + Assert.That(normal.GetProperty("campaign_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(normal.GetProperty("img").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(normal.GetProperty("reward").GetArrayLength(), Is.EqualTo(15)); + + var firstReward = normal.GetProperty("reward")[0]; + Assert.That(firstReward.GetProperty("reward_type").GetString(), Is.EqualTo("9")); + Assert.That(firstReward.GetProperty("reward_number").GetString(), Is.EqualTo("20")); + + Assert.That(dlb.GetProperty("campaign").ValueKind, Is.EqualTo(JsonValueKind.Array)); + Assert.That(dlb.GetProperty("campaign").GetArrayLength(), Is.EqualTo(0)); + } + + [Test] + public async Task Index_omits_daily_login_bonus_on_second_call_same_day() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + + await PostIndexAndReadBody(factory, viewerId); + var root = await PostIndexAndReadBody(factory, viewerId); + + var present = root.TryGetProperty("daily_login_bonus", out var dlb) + && dlb.ValueKind != JsonValueKind.Null; + Assert.That(present, Is.False, "Second-same-day /load/index must not re-emit the bonus"); + } + [Test] public async Task LoadIndex_emits_battle_pass_level_info_with_100_entries_when_period_active() { From 0d2da66795ff60f2c6836212fe43a7bb92e941ae Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 16:50:17 -0400 Subject: [PATCH 9/9] feat(mypage): can_give_daily_login_bonus reflects IsDue(viewer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Injects ILoginBonusService into MyPageController and sets CanGiveDailyLoginBonus from IsDue(viewer) in the Index response. Adds two integration tests: fresh viewer → true, post-load-index → false. Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/MyPageController.cs | 5 +- .../Controllers/MyPageControllerTests.cs | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 SVSim.UnitTests/Controllers/MyPageControllerTests.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs b/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs index a94d4491..8c61c683 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs @@ -30,16 +30,18 @@ public class MyPageController : SVSimController private readonly IGameConfigService _config; private readonly IArenaTwoPickRunRepository _arenaTwoPickRuns; private readonly IHomeDialogSessionTracker _homeDialogTracker; + private readonly ILoginBonusService _loginBonus; public MyPageController(IViewerRepository viewerRepository, IGlobalsRepository globalsRepository, IGameConfigService config, IArenaTwoPickRunRepository arenaTwoPickRuns, - IHomeDialogSessionTracker homeDialogTracker) + IHomeDialogSessionTracker homeDialogTracker, ILoginBonusService loginBonus) { _viewerRepository = viewerRepository; _globalsRepository = globalsRepository; _config = config; _arenaTwoPickRuns = arenaTwoPickRuns; _homeDialogTracker = homeDialogTracker; + _loginBonus = loginBonus; } [HttpPost("index")] @@ -83,6 +85,7 @@ public class MyPageController : SVSimController return new MyPageIndexResponse { UserInfo = new UserInfo(deviceType, viewer), + CanGiveDailyLoginBonus = _loginBonus.IsDue(viewer), UnreceivedMissionRewardCount = 0, // TODO(mypage-stub): viewer mission progress ReceiveFriendApplyCount = 0, // TODO(mypage-stub): viewer friend-request inbox UnreadPresentCount = 0, // TODO(mypage-stub): viewer presents/mail diff --git a/SVSim.UnitTests/Controllers/MyPageControllerTests.cs b/SVSim.UnitTests/Controllers/MyPageControllerTests.cs new file mode 100644 index 00000000..a3fb9733 --- /dev/null +++ b/SVSim.UnitTests/Controllers/MyPageControllerTests.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using NUnit.Framework; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Controllers; + +/// +/// Coverage for /mypage/index. Focused on fields computed from viewer state +/// that are easy to regress (can_give_daily_login_bonus). +/// +public class MyPageControllerTests +{ + // MyPageIndexRequest extends BaseRequest, so viewer_id + steam_session_ticket are required. + private const string MyPageRequestJson = + """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam"}"""; + + private const string LoadIndexRequestJson = + """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam","card_master_hash":""}"""; + + [Test] + public async Task MyPage_can_give_daily_login_bonus_is_true_for_fresh_viewer() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/mypage/index", + new StringContent(MyPageRequestJson, Encoding.UTF8, "application/json")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK), await resp.Content.ReadAsStringAsync()); + + var root = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement; + Assert.That(root.GetProperty("can_give_daily_login_bonus").GetBoolean(), Is.True); + } + + [Test] + public async Task MyPage_can_give_daily_login_bonus_is_false_after_load_index_claim() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var client = factory.CreateAuthenticatedClient(viewerId); + + // /load/index claims today's bonus + var loadResp = await client.PostAsync("/load/index", + new StringContent(LoadIndexRequestJson, Encoding.UTF8, "application/json")); + Assert.That(loadResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), await loadResp.Content.ReadAsStringAsync()); + + // /mypage/index must report flag = false after the claim + var resp = await client.PostAsync("/mypage/index", + new StringContent(MyPageRequestJson, Encoding.UTF8, "application/json")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK), await resp.Content.ReadAsStringAsync()); + + var root = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement; + Assert.That(root.GetProperty("can_give_daily_login_bonus").GetBoolean(), Is.False); + } +}