refactor(tutorial-presents): promote static catalogue to seed-driven TutorialPresentEntries table
The five tutorial gifts every fresh viewer is given at signup used to live as a static C# array in SVSim.Database/SeedData/TutorialPresents.cs — outside the seed-JSON pipeline used by all 40+ other globals tables. Editing a gift required a code change + rebuild instead of a JSON edit + bootstrap re-run. Now authored in SVSim.Bootstrap/Data/seeds/tutorial-presents.json and loaded into a new TutorialPresentEntries table via TutorialPresentsImporter (clear-and-rewrite, mirroring HomeDialogs). ViewerRepository.RegisterAnonymousViewer reads the table at signup and projects each row into a ViewerPresent with Source="tutorial". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
4244
SVSim.Database/Migrations/20260609132350_AddTutorialPresentEntries.Designer.cs
generated
Normal file
4244
SVSim.Database/Migrations/20260609132350_AddTutorialPresentEntries.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTutorialPresentEntries : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TutorialPresentEntries",
|
||||
columns: table => new
|
||||
{
|
||||
PresentId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
RewardType = table.Column<int>(type: "integer", nullable: false),
|
||||
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
|
||||
RewardCount = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemType = table.Column<int>(type: "integer", nullable: true),
|
||||
Message = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TutorialPresentEntries", x => x.PresentId);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TutorialPresentEntries");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2530,6 +2530,33 @@ namespace SVSim.Database.Migrations
|
||||
b.ToTable("StoryDecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.TutorialPresentEntry", b =>
|
||||
{
|
||||
b.Property<string>("PresentId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<int?>("ItemType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("RewardCount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("RewardDetailId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("RewardType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("PresentId");
|
||||
|
||||
b.ToTable("TutorialPresentEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
18
SVSim.Database/Models/TutorialPresentEntry.cs
Normal file
18
SVSim.Database/Models/TutorialPresentEntry.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One row in the tutorial-gift catalogue every fresh viewer is given at signup. Authored in
|
||||
/// <c>SVSim.Bootstrap/Data/seeds/tutorial-presents.json</c>; <see cref="PresentId"/> is the
|
||||
/// wire-stable identifier and serves as the primary key. <c>ViewerRepository.RegisterAnonymousViewer</c>
|
||||
/// reads this table and projects each row into a <see cref="ViewerPresent"/> with Source="tutorial".
|
||||
/// </summary>
|
||||
public class TutorialPresentEntry
|
||||
{
|
||||
public string PresentId { get; set; } = string.Empty;
|
||||
|
||||
public int RewardType { get; set; }
|
||||
public long RewardDetailId { get; set; }
|
||||
public long RewardCount { get; set; }
|
||||
public int? ItemType { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using Npgsql;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.SeedData;
|
||||
using SVSim.Database.Services;
|
||||
|
||||
namespace SVSim.Database.Repositories.Viewer;
|
||||
@@ -114,10 +113,17 @@ public class ViewerRepository : IViewerRepository
|
||||
|
||||
// Eager-seed the tutorial gifts so the inbox is populated by the time the tutorial
|
||||
// walks the user to it (which happens AFTER initial battles, per the gift-inbox
|
||||
// design). The unique (ViewerId, PresentId) index is the backstop against
|
||||
// double-seeding on a retried signup. Both inserts commit in a single SaveChanges.
|
||||
// design). The catalogue lives in TutorialPresentEntries (loaded from
|
||||
// SVSim.Bootstrap/Data/seeds/tutorial-presents.json); if the catalogue is empty
|
||||
// (bootstrap not run) signup still succeeds with an empty inbox. The unique
|
||||
// (ViewerId, PresentId) index is the backstop against double-seeding on a retried
|
||||
// signup. Both inserts commit in a single SaveChanges.
|
||||
var tutorialPresents = await _dbContext.Set<TutorialPresentEntry>()
|
||||
.AsNoTracking()
|
||||
.OrderBy(p => p.PresentId)
|
||||
.ToListAsync();
|
||||
var createdAt = DateTime.UtcNow;
|
||||
foreach (var spec in TutorialPresents.All)
|
||||
foreach (var spec in tutorialPresents)
|
||||
{
|
||||
_dbContext.Set<ViewerPresent>().Add(new ViewerPresent
|
||||
{
|
||||
|
||||
@@ -102,6 +102,7 @@ public class SVSimDbContext : DbContext
|
||||
public DbSet<ViewerStoryBranchUnlock> ViewerStoryBranchUnlocks => Set<ViewerStoryBranchUnlock>();
|
||||
|
||||
public DbSet<ViewerPresent> ViewerPresents => Set<ViewerPresent>();
|
||||
public DbSet<TutorialPresentEntry> TutorialPresentEntries => Set<TutorialPresentEntry>();
|
||||
|
||||
public DbSet<ArenaTwoPickReward> ArenaTwoPickRewards { get; set; } = null!;
|
||||
public DbSet<ViewerArenaTwoPickRun> ViewerArenaTwoPickRuns { get; set; } = null!;
|
||||
@@ -391,6 +392,12 @@ public class SVSimDbContext : DbContext
|
||||
b.HasIndex(p => new { p.ViewerId, p.PresentId }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TutorialPresentEntry>(b =>
|
||||
{
|
||||
b.HasKey(p => p.PresentId);
|
||||
b.Property(p => p.PresentId).HasMaxLength(64);
|
||||
});
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
namespace SVSim.Database.SeedData;
|
||||
|
||||
/// <summary>
|
||||
/// The five tutorial gifts every fresh viewer is given at signup. Mirrors the prod-shaped
|
||||
/// gift inbox — these rows are indistinguishable from server-issued gifts at read time
|
||||
/// except for the Source tag.
|
||||
///
|
||||
/// Moved here from the old static <c>GiftController.TutorialGifts</c> PresentDto array;
|
||||
/// now insert-only data, not DTOs, so signup writes go straight into ViewerPresent rows
|
||||
/// without round-tripping through wire types.
|
||||
/// </summary>
|
||||
public static class TutorialPresents
|
||||
{
|
||||
public record Spec(
|
||||
string PresentId,
|
||||
int RewardType,
|
||||
long RewardDetailId,
|
||||
long RewardCount,
|
||||
int? ItemType,
|
||||
string Message);
|
||||
|
||||
public static readonly IReadOnlyList<Spec> All = new[]
|
||||
{
|
||||
new Spec("71478626", 1, 0, 400, null, "For completing the tutorial"), // Crystal
|
||||
new Spec("71478627", 9, 0, 100, null, "For completing the tutorial"), // Rupy
|
||||
new Spec("71478628", 4, 1, 3, 1, "For completing the tutorial"), // Item type=1, x3 of item 1
|
||||
new Spec("71478629", 4, 80001, 40, 2, "For completing the tutorial"), // Pack ticket 80001 x40
|
||||
new Spec("71478630", 4, 90001, 1, 2, "For completing the tutorial"), // Pack ticket 90001 x1
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user