From ee808a60a2e65ad1b4b9d0f2e9db0fb1a69f66cc Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 9 Jun 2026 16:26:48 -0400 Subject: [PATCH] feat(viewer): add MyPageBgSelectType + MyPageBgId scalars + MyPageBgRotation owned collection Adds BGType persistence (0=Deck/1=CustomBG/2=RandomBG) to Viewer via two scalar columns and an owned collection keyed (ViewerId, Slot). Two persistence tests confirm round-trip and zero-defaults on fresh viewers. Co-Authored-By: Claude Sonnet 4.6 --- .../Models/MyPageBgRotationEntry.cs | 16 +++++ SVSim.Database/Models/Viewer.cs | 8 +++ SVSim.Database/SVSimDbContext.cs | 6 ++ .../Persistence/MyPageBgPersistenceTests.cs | 61 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 SVSim.Database/Models/MyPageBgRotationEntry.cs create mode 100644 SVSim.UnitTests/Persistence/MyPageBgPersistenceTests.cs diff --git a/SVSim.Database/Models/MyPageBgRotationEntry.cs b/SVSim.Database/Models/MyPageBgRotationEntry.cs new file mode 100644 index 0000000..50cb7d5 --- /dev/null +++ b/SVSim.Database/Models/MyPageBgRotationEntry.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; + +namespace SVSim.Database.Models; + +/// +/// One row per (viewer, slot) in the viewer's saved MyPage BG rotation pool. The client posts +/// the full pool on every /user_mypage/update regardless of mode, so the server overwrites +/// it atomically each time. Slot is the 0-based position; order is preserved for the +/// /mypage/index echo. +/// +[Owned] +public class MyPageBgRotationEntry +{ + public int Slot { get; set; } + public int BgId { get; set; } +} diff --git a/SVSim.Database/Models/Viewer.cs b/SVSim.Database/Models/Viewer.cs index 4b50597..59861db 100644 --- a/SVSim.Database/Models/Viewer.cs +++ b/SVSim.Database/Models/Viewer.cs @@ -33,6 +33,12 @@ public class Viewer : BaseEntity public DateTime LastLogin { get; set; } + /// BGType enum: 0=Deck, 1=CustomBG, 2=RandomBG. Default 0 = follow equipped deck's leader skin. + public int MyPageBgSelectType { get; set; } + + /// The single chosen MyPageBG cosmetic id, used when SelectType=CustomBG. 0 = none. + public int MyPageBgId { get; set; } + #region Owned public ViewerInfo Info { get; set; } = new ViewerInfo(); @@ -67,6 +73,8 @@ public class Viewer : BaseEntity public List FreePackClaims { get; set; } = new List(); + public List MyPageBgRotation { get; set; } = new List(); + public List GachaPointBalances { get; set; } = new List(); public List GachaPointReceived { get; set; } = new List(); diff --git a/SVSim.Database/SVSimDbContext.cs b/SVSim.Database/SVSimDbContext.cs index ee226b2..0df87cf 100644 --- a/SVSim.Database/SVSimDbContext.cs +++ b/SVSim.Database/SVSimDbContext.cs @@ -172,6 +172,12 @@ public class SVSimDbContext : DbContext b.HasKey("ViewerId", nameof(ViewerFreePackClaim.FreeGachaCampaignId)); b.Property(x => x.FreeGachaCampaignId).ValueGeneratedNever(); }); + modelBuilder.Entity().OwnsMany(v => v.MyPageBgRotation, b => + { + b.WithOwner().HasForeignKey("ViewerId"); + b.HasKey("ViewerId", nameof(MyPageBgRotationEntry.Slot)); + b.Property(x => x.Slot).ValueGeneratedNever(); + }); // OwnedCardEntry and OwnedItemEntry use composite PK (ViewerId, Id) where Id is auto- // generated, which silently permits multiple rows per (Viewer, Card) or (Viewer, Item). diff --git a/SVSim.UnitTests/Persistence/MyPageBgPersistenceTests.cs b/SVSim.UnitTests/Persistence/MyPageBgPersistenceTests.cs new file mode 100644 index 0000000..3435fc8 --- /dev/null +++ b/SVSim.UnitTests/Persistence/MyPageBgPersistenceTests.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Models; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Persistence; + +public class MyPageBgPersistenceTests +{ + [Test] + public async Task Viewer_round_trips_mypage_bg_scalars_and_rotation_pool() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + + using (var seedScope = factory.Services.CreateScope()) + { + var ctx = seedScope.ServiceProvider.GetRequiredService(); + var viewer = await ctx.Viewers + .Include(v => v.MyPageBgRotation) + .FirstAsync(v => v.Id == viewerId); + viewer.MyPageBgSelectType = 2; + viewer.MyPageBgId = 1213410310; + viewer.MyPageBgRotation.Add(new MyPageBgRotationEntry { Slot = 0, BgId = 1211410310 }); + viewer.MyPageBgRotation.Add(new MyPageBgRotationEntry { Slot = 1, BgId = 1212410310 }); + viewer.MyPageBgRotation.Add(new MyPageBgRotationEntry { Slot = 2, BgId = 1213410310 }); + await ctx.SaveChangesAsync(); + } + + using var verifyScope = factory.Services.CreateScope(); + var ctx2 = verifyScope.ServiceProvider.GetRequiredService(); + var roundtrip = await ctx2.Viewers + .Include(v => v.MyPageBgRotation) + .AsNoTracking() + .FirstAsync(v => v.Id == viewerId); + + Assert.That(roundtrip.MyPageBgSelectType, Is.EqualTo(2)); + Assert.That(roundtrip.MyPageBgId, Is.EqualTo(1213410310)); + Assert.That(roundtrip.MyPageBgRotation.OrderBy(r => r.Slot).Select(r => r.BgId), + Is.EqualTo(new[] { 1211410310, 1212410310, 1213410310 })); + } + + [Test] + public async Task Viewer_fresh_viewer_has_zero_defaults_and_empty_rotation() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + + using var scope = factory.Services.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var viewer = await ctx.Viewers + .Include(v => v.MyPageBgRotation) + .AsNoTracking() + .FirstAsync(v => v.Id == viewerId); + + Assert.That(viewer.MyPageBgSelectType, Is.EqualTo(0)); + Assert.That(viewer.MyPageBgId, Is.EqualTo(0)); + Assert.That(viewer.MyPageBgRotation, Is.Empty); + } +}