Pack logic cleanup
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using SVSim.Database.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.DataSeeders;
|
||||
|
||||
@@ -8,21 +7,10 @@ public class DefaultSettingsSeeder : IDataSeeder
|
||||
{
|
||||
public void Seed(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<GameConfiguration>().HasData(new GameConfiguration
|
||||
{
|
||||
Id = "default",
|
||||
DefaultCrystals = 50000,
|
||||
DefaultRupees = 50000,
|
||||
DefaultEther = 50000,
|
||||
MaxFriends = 20,
|
||||
DefaultEmblemId = 100000000,
|
||||
DefaultDegreeId = 300003,
|
||||
DefaultMyPageBackgroundId = 100000000,
|
||||
DefaultSleeveId = 3000011,
|
||||
DefaultDegree = null,
|
||||
DefaultEmblem = null,
|
||||
DefaultSleeve = null,
|
||||
DefaultMyPageBackground = null
|
||||
});
|
||||
// GameConfiguration is seeded at runtime (see SVSimDbContext.EnsureSeedDataAsync),
|
||||
// NOT via HasData — EF Core 8's HasData+OwnsOne(...).ToJson combo doesn't reliably
|
||||
// populate the jsonb cell, leading to NOT NULL violations on a fresh DB. Runtime
|
||||
// seeding writes a full `GameConfigRoot()` (all sub-config POCO initialisers
|
||||
// produce the canonical defaults).
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34971
SVSim.Database/Migrations/20260524065010_RefactorGameConfigurationToJsonb.Designer.cs
generated
Normal file
34971
SVSim.Database/Migrations/20260524065010_RefactorGameConfigurationToJsonb.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RefactorGameConfigurationToJsonb : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Step 1: Add the new jsonb column FIRST so the backfill SQL can read the old columns.
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Config",
|
||||
table: "GameConfigurations",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
// Step 2: Backfill — project old flat columns into the new jsonb document.
|
||||
// For fresh-install databases (no pre-existing rows) this is a no-op; the
|
||||
// DefaultSettingsSeeder InsertData in the previous migration already seeded the
|
||||
// parent row, so the UPDATE below covers it. PackRates defaults are hard-coded
|
||||
// here because the old schema had no equivalent columns.
|
||||
// Use provider-specific SQL: PostgreSQL jsonb functions vs SQLite json functions.
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE "GameConfigurations"
|
||||
SET "Config" = jsonb_build_object(
|
||||
'DefaultGrants', jsonb_build_object(
|
||||
'Crystals', "DefaultCrystals",
|
||||
'Rupees', "DefaultRupees",
|
||||
'Ether', "DefaultEther"
|
||||
),
|
||||
'Player', jsonb_build_object(
|
||||
'MaxFriends', "MaxFriends"
|
||||
),
|
||||
'DefaultLoadout', jsonb_build_object(
|
||||
'DegreeId', "DefaultDegreeId",
|
||||
'EmblemId', "DefaultEmblemId",
|
||||
'MyPageBackgroundId', "DefaultMyPageBackgroundId",
|
||||
'SleeveId', "DefaultSleeveId"
|
||||
),
|
||||
'Challenge', jsonb_build_object(
|
||||
'UseTwoPickPremiumCard', "ChallengeUseTwoPickPremiumCard",
|
||||
'TwoPickSleeveId', "ChallengeTwoPickSleeveId"
|
||||
),
|
||||
'Rotation', jsonb_build_object(
|
||||
'TsRotationId', "TsRotationId",
|
||||
'IsBattlePassPeriod', "IsBattlePassPeriod",
|
||||
'IsBeginnerMission', "IsBeginnerMission",
|
||||
'CardSetIdForResourceDlView', "CardSetIdForResourceDlView"
|
||||
),
|
||||
'PackRates', jsonb_build_object(
|
||||
'AnimatedRate', 0.08,
|
||||
'Default', jsonb_build_object('Bronze', 0.6744, 'Silver', 0.25, 'Gold', 0.06, 'Legendary', 0.015),
|
||||
'PerSlot', jsonb_build_array(
|
||||
jsonb_build_object('Slot', '8', 'Bronze', 0, 'Silver', 0.7692, 'Gold', 0.1846, 'Legendary', 0.0462)
|
||||
)
|
||||
)
|
||||
)
|
||||
WHERE "Id" = 'default';
|
||||
""",
|
||||
suppressTransaction: true);
|
||||
|
||||
// For SQLite (tests): use json() and json_object() functions.
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE "GameConfigurations"
|
||||
SET "Config" = json('{"DefaultGrants":{"Crystals":'||COALESCE("DefaultCrystals", 0)||',"Rupees":'||COALESCE("DefaultRupees", 0)||',"Ether":'||COALESCE("DefaultEther", 0)||'},'||
|
||||
'"Player":{"MaxFriends":'||COALESCE("MaxFriends", 0)||'},'||
|
||||
'"DefaultLoadout":{"DegreeId":'||COALESCE("DefaultDegreeId", 0)||',"EmblemId":'||COALESCE("DefaultEmblemId", 0)||',"MyPageBackgroundId":'||COALESCE("DefaultMyPageBackgroundId", 0)||',"SleeveId":'||COALESCE("DefaultSleeveId", 0)||'},'||
|
||||
'"Challenge":{"UseTwoPickPremiumCard":'||(CASE WHEN "ChallengeUseTwoPickPremiumCard" THEN 'true' ELSE 'false' END)||',"TwoPickSleeveId":'||COALESCE("ChallengeTwoPickSleeveId", 0)||'},'||
|
||||
'"Rotation":{"TsRotationId":"'||COALESCE("TsRotationId", '')||'","IsBattlePassPeriod":'||(CASE WHEN "IsBattlePassPeriod" THEN 'true' ELSE 'false' END)||',"IsBeginnerMission":'||(CASE WHEN "IsBeginnerMission" THEN 'true' ELSE 'false' END)||',"CardSetIdForResourceDlView":'||COALESCE("CardSetIdForResourceDlView", 0)||'},'||
|
||||
'"PackRates":{"AnimatedRate":0.08,"Default":{"Bronze":0.6744,"Silver":0.25,"Gold":0.06,"Legendary":0.015},"PerSlot":[{"Slot":"8","Bronze":0,"Silver":0.7692,"Gold":0.1846,"Legendary":0.0462}]}'||
|
||||
'}')
|
||||
WHERE "Id" = 'default';
|
||||
""",
|
||||
suppressTransaction: true);
|
||||
|
||||
|
||||
// Step 3: Drop FK constraints, indexes, and old flat columns.
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GameConfigurations_Degrees_DefaultDegreeId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GameConfigurations_Emblems_DefaultEmblemId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GameConfigurations_MyPageBackgrounds_DefaultMyPageBackgroun~",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GameConfigurations_Sleeves_DefaultSleeveId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GameConfigurations_DefaultDegreeId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GameConfigurations_DefaultEmblemId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GameConfigurations_DefaultMyPageBackgroundId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GameConfigurations_DefaultSleeveId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CardSetIdForResourceDlView",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChallengeTwoPickSleeveId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChallengeUseTwoPickPremiumCard",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultCrystals",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultDegreeId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultEmblemId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultEther",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultMyPageBackgroundId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultRupees",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultSleeveId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsBattlePassPeriod",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsBeginnerMission",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxFriends",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TsRotationId",
|
||||
table: "GameConfigurations");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Config",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CardSetIdForResourceDlView",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "ChallengeTwoPickSleeveId",
|
||||
table: "GameConfigurations",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ChallengeUseTwoPickPremiumCard",
|
||||
table: "GameConfigurations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "DefaultCrystals",
|
||||
table: "GameConfigurations",
|
||||
type: "numeric(20,0)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DefaultDegreeId",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DefaultEmblemId",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "DefaultEther",
|
||||
table: "GameConfigurations",
|
||||
type: "numeric(20,0)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DefaultMyPageBackgroundId",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "DefaultRupees",
|
||||
table: "GameConfigurations",
|
||||
type: "numeric(20,0)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DefaultSleeveId",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsBattlePassPeriod",
|
||||
table: "GameConfigurations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsBeginnerMission",
|
||||
table: "GameConfigurations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxFriends",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TsRotationId",
|
||||
table: "GameConfigurations",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "GameConfigurations",
|
||||
keyColumn: "Id",
|
||||
keyValue: "default",
|
||||
columns: new[] { "CardSetIdForResourceDlView", "ChallengeTwoPickSleeveId", "ChallengeUseTwoPickPremiumCard", "DefaultCrystals", "DefaultDegreeId", "DefaultEmblemId", "DefaultEther", "DefaultMyPageBackgroundId", "DefaultRupees", "DefaultSleeveId", "IsBattlePassPeriod", "IsBeginnerMission", "MaxFriends", "TsRotationId" },
|
||||
values: new object[] { 0, 0L, false, 50000m, 300003, 100000000, 50000m, 100000000, 50000m, 3000011, false, false, 20, "" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GameConfigurations_DefaultDegreeId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultDegreeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GameConfigurations_DefaultEmblemId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultEmblemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GameConfigurations_DefaultMyPageBackgroundId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultMyPageBackgroundId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GameConfigurations_DefaultSleeveId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultSleeveId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GameConfigurations_Degrees_DefaultDegreeId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultDegreeId",
|
||||
principalTable: "Degrees",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GameConfigurations_Emblems_DefaultEmblemId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultEmblemId",
|
||||
principalTable: "Emblems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GameConfigurations_MyPageBackgrounds_DefaultMyPageBackgroun~",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultMyPageBackgroundId",
|
||||
principalTable: "MyPageBackgrounds",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GameConfigurations_Sleeves_DefaultSleeveId",
|
||||
table: "GameConfigurations",
|
||||
column: "DefaultSleeveId",
|
||||
principalTable: "Sleeves",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
34967
SVSim.Database/Migrations/20260524071358_AddIsFoilToCards.Designer.cs
generated
Normal file
34967
SVSim.Database/Migrations/20260524071358_AddIsFoilToCards.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
SVSim.Database/Migrations/20260524071358_AddIsFoilToCards.cs
Normal file
29
SVSim.Database/Migrations/20260524071358_AddIsFoilToCards.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsFoilToCards : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsFoil",
|
||||
table: "Cards",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsFoil",
|
||||
table: "Cards");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21730,87 +21730,15 @@ namespace SVSim.Database.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("CardSetIdForResourceDlView")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("ChallengeTwoPickSleeveId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("ChallengeUseTwoPickPremiumCard")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal>("DefaultCrystals")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<int>("DefaultDegreeId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("DefaultEmblemId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("DefaultEther")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<int>("DefaultMyPageBackgroundId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("DefaultRupees")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<int>("DefaultSleeveId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsBattlePassPeriod")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsBeginnerMission")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("MaxFriends")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TsRotationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefaultDegreeId");
|
||||
|
||||
b.HasIndex("DefaultEmblemId");
|
||||
|
||||
b.HasIndex("DefaultMyPageBackgroundId");
|
||||
|
||||
b.HasIndex("DefaultSleeveId");
|
||||
|
||||
b.ToTable("GameConfigurations");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = "default",
|
||||
CardSetIdForResourceDlView = 0,
|
||||
ChallengeTwoPickSleeveId = 0L,
|
||||
ChallengeUseTwoPickPremiumCard = false,
|
||||
DateCreated = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
DefaultCrystals = 50000m,
|
||||
DefaultDegreeId = 300003,
|
||||
DefaultEmblemId = 100000000,
|
||||
DefaultEther = 50000m,
|
||||
DefaultMyPageBackgroundId = 100000000,
|
||||
DefaultRupees = 50000m,
|
||||
DefaultSleeveId = 3000011,
|
||||
IsBattlePassPeriod = false,
|
||||
IsBeginnerMission = false,
|
||||
MaxFriends = 20,
|
||||
TsRotationId = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.ItemEntry", b =>
|
||||
@@ -26427,6 +26355,9 @@ namespace SVSim.Database.Migrations
|
||||
b.Property<int?>("Defense")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsFoil")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -34232,37 +34163,230 @@ namespace SVSim.Database.Migrations
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.GameConfiguration", b =>
|
||||
{
|
||||
b.HasOne("SVSim.Database.Models.DegreeEntry", "DefaultDegree")
|
||||
.WithMany()
|
||||
.HasForeignKey("DefaultDegreeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
b.OwnsOne("SVSim.Database.Models.GameConfigRoot", "Config", b1 =>
|
||||
{
|
||||
b1.Property<string>("GameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("GameConfigurationId");
|
||||
|
||||
b1.ToTable("GameConfigurations");
|
||||
|
||||
b1.ToJson("Config");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("GameConfigurationId");
|
||||
|
||||
b1.OwnsOne("SVSim.Database.Models.Config.ChallengeConfig", "Challenge", b2 =>
|
||||
{
|
||||
b2.Property<string>("GameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.Property<long>("TwoPickSleeveId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b2.Property<bool>("UseTwoPickPremiumCard")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b2.HasKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.ToTable("GameConfigurations");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("GameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b1.OwnsOne("SVSim.Database.Models.Config.DefaultGrantsConfig", "DefaultGrants", b2 =>
|
||||
{
|
||||
b2.Property<string>("GameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.Property<decimal>("Crystals")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b2.Property<decimal>("Ether")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b2.Property<decimal>("Rupees")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b2.HasKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.ToTable("GameConfigurations");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("GameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b1.OwnsOne("SVSim.Database.Models.Config.DefaultLoadoutConfig", "DefaultLoadout", b2 =>
|
||||
{
|
||||
b2.Property<string>("GameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.Property<int>("DegreeId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.Property<int>("EmblemId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.Property<int>("MyPageBackgroundId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.Property<int>("SleeveId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.HasKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.ToTable("GameConfigurations");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("GameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b1.OwnsOne("SVSim.Database.Models.Config.PackRateConfig", "PackRates", b2 =>
|
||||
{
|
||||
b2.Property<string>("GameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.Property<double>("AnimatedRate")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b2.HasKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.ToTable("GameConfigurations");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.OwnsOne("SVSim.Database.Models.Config.SlotRarityWeights", "Default", b3 =>
|
||||
{
|
||||
b3.Property<string>("PackRateConfigGameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b3.Property<double>("Bronze")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<double>("Gold")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<double>("Legendary")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<double>("Silver")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<string>("Slot")
|
||||
.HasColumnType("text");
|
||||
|
||||
b3.HasKey("PackRateConfigGameConfigRootGameConfigurationId");
|
||||
|
||||
b3.ToTable("GameConfigurations");
|
||||
|
||||
b3.WithOwner()
|
||||
.HasForeignKey("PackRateConfigGameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b2.OwnsMany("SVSim.Database.Models.Config.SlotRarityWeights", "PerSlot", b3 =>
|
||||
{
|
||||
b3.Property<string>("PackRateConfigGameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b3.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b3.Property<double>("Bronze")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<double>("Gold")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<double>("Legendary")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<double>("Silver")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b3.Property<string>("Slot")
|
||||
.HasColumnType("text");
|
||||
|
||||
b3.HasKey("PackRateConfigGameConfigRootGameConfigurationId", "Id");
|
||||
|
||||
b3.ToTable("GameConfigurations");
|
||||
|
||||
b3.WithOwner()
|
||||
.HasForeignKey("PackRateConfigGameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b2.Navigation("Default")
|
||||
.IsRequired();
|
||||
|
||||
b2.Navigation("PerSlot");
|
||||
});
|
||||
|
||||
b1.OwnsOne("SVSim.Database.Models.Config.PlayerConfig", "Player", b2 =>
|
||||
{
|
||||
b2.Property<string>("GameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.Property<int>("MaxFriends")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.HasKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.ToTable("GameConfigurations");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("GameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b1.OwnsOne("SVSim.Database.Models.Config.RotationConfig", "Rotation", b2 =>
|
||||
{
|
||||
b2.Property<string>("GameConfigRootGameConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.Property<int>("CardSetIdForResourceDlView")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.Property<bool>("IsBattlePassPeriod")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b2.Property<bool>("IsBeginnerMission")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b2.Property<string>("TsRotationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b2.HasKey("GameConfigRootGameConfigurationId");
|
||||
|
||||
b2.ToTable("GameConfigurations");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("GameConfigRootGameConfigurationId");
|
||||
});
|
||||
|
||||
b1.Navigation("Challenge")
|
||||
.IsRequired();
|
||||
|
||||
b1.Navigation("DefaultGrants")
|
||||
.IsRequired();
|
||||
|
||||
b1.Navigation("DefaultLoadout")
|
||||
.IsRequired();
|
||||
|
||||
b1.Navigation("PackRates")
|
||||
.IsRequired();
|
||||
|
||||
b1.Navigation("Player")
|
||||
.IsRequired();
|
||||
|
||||
b1.Navigation("Rotation")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b.Navigation("Config")
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SVSim.Database.Models.EmblemEntry", "DefaultEmblem")
|
||||
.WithMany()
|
||||
.HasForeignKey("DefaultEmblemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SVSim.Database.Models.MyPageBackgroundEntry", "DefaultMyPageBackground")
|
||||
.WithMany()
|
||||
.HasForeignKey("DefaultMyPageBackgroundId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SVSim.Database.Models.SleeveEntry", "DefaultSleeve")
|
||||
.WithMany()
|
||||
.HasForeignKey("DefaultSleeveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DefaultDegree");
|
||||
|
||||
b.Navigation("DefaultEmblem");
|
||||
|
||||
b.Navigation("DefaultMyPageBackground");
|
||||
|
||||
b.Navigation("DefaultSleeve");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b =>
|
||||
|
||||
10
SVSim.Database/Models/Config/ChallengeConfig.cs
Normal file
10
SVSim.Database/Models/Config/ChallengeConfig.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
[Owned]
|
||||
public class ChallengeConfig
|
||||
{
|
||||
public bool UseTwoPickPremiumCard { get; set; }
|
||||
public long TwoPickSleeveId { get; set; }
|
||||
}
|
||||
12
SVSim.Database/Models/Config/DefaultGrantsConfig.cs
Normal file
12
SVSim.Database/Models/Config/DefaultGrantsConfig.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
/// <summary>Per-viewer-registration default currency grants.</summary>
|
||||
[Owned]
|
||||
public class DefaultGrantsConfig
|
||||
{
|
||||
public ulong Crystals { get; set; } = 50000;
|
||||
public ulong Rupees { get; set; } = 50000;
|
||||
public ulong Ether { get; set; } = 50000;
|
||||
}
|
||||
17
SVSim.Database/Models/Config/DefaultLoadoutConfig.cs
Normal file
17
SVSim.Database/Models/Config/DefaultLoadoutConfig.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Default cosmetic loadout ids for a newly-registered viewer. These used to be FK columns;
|
||||
/// they're now untyped longs in the jsonb tree. Validation would live in a future config-editing
|
||||
/// UI (see project-wide TODO(config-validation)).
|
||||
/// </summary>
|
||||
[Owned]
|
||||
public class DefaultLoadoutConfig
|
||||
{
|
||||
public int DegreeId { get; set; } = 300003;
|
||||
public int EmblemId { get; set; } = 100000000;
|
||||
public int MyPageBackgroundId { get; set; } = 100000000;
|
||||
public int SleeveId { get; set; } = 3000011;
|
||||
}
|
||||
47
SVSim.Database/Models/Config/PackRateConfig.cs
Normal file
47
SVSim.Database/Models/Config/PackRateConfig.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Tunables for pack-opening RNG. Defaults reproduce the original Shadowverse Classic rates
|
||||
/// exactly so the cutover from hardcoded magic numbers is zero-behavior-change.
|
||||
/// </summary>
|
||||
[Owned]
|
||||
public class PackRateConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-card-slot probability of upgrading any drawn card to its foil/animated twin.
|
||||
/// Applied AFTER rarity selection — independent of rarity, slot position, and pack category.
|
||||
/// Default 0.08 (8%). Cards without a foil twin in master data keep the non-foil silently.
|
||||
/// </summary>
|
||||
public double AnimatedRate { get; set; } = 0.08;
|
||||
|
||||
/// <summary>
|
||||
/// Global default rarity weights, used for any slot that has no entry in
|
||||
/// <see cref="PerSlot"/>. Defaults match SV Classic main-slot. Weights sum to 0.9994;
|
||||
/// the 0.06% slack absorbs into Bronze via the PickRarity catch-all band.
|
||||
/// </summary>
|
||||
public SlotRarityWeights Default { get; set; } = new()
|
||||
{
|
||||
Bronze = 0.6744, Silver = 0.25, Gold = 0.06, Legendary = 0.015,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Per-slot overrides (1-based slot index) applied to all packs. A missing slot falls back
|
||||
/// to <see cref="Default"/>. Each entry is a FULL OVERRIDE, not a delta — if you change
|
||||
/// <see cref="Default"/>, existing PerSlot entries do NOT auto-recompute. The slot-8 default
|
||||
/// expresses the SV Classic "Silver-or-better guarantee" as data (Bronze=0) instead of a
|
||||
/// separate code path.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Per-slot overrides keyed by 1-based slot index (stored as a list for EF Core 8 json
|
||||
/// compatibility — Dictionary<string,T> of complex owned types is not supported).
|
||||
/// Look up by <see cref="SlotRarityWeights.Slot"/>. A missing slot falls back to
|
||||
/// <see cref="Default"/>. Slot-8 entry expresses the SV Classic "Silver-or-better
|
||||
/// guarantee" as data (Bronze=0).
|
||||
/// </summary>
|
||||
public List<SlotRarityWeights> PerSlot { get; set; } =
|
||||
[
|
||||
new() { Slot = "8", Bronze = 0, Silver = 0.7692, Gold = 0.1846, Legendary = 0.0462 },
|
||||
];
|
||||
}
|
||||
9
SVSim.Database/Models/Config/PlayerConfig.cs
Normal file
9
SVSim.Database/Models/Config/PlayerConfig.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
[Owned]
|
||||
public class PlayerConfig
|
||||
{
|
||||
public int MaxFriends { get; set; } = 20;
|
||||
}
|
||||
15
SVSim.Database/Models/Config/RotationConfig.cs
Normal file
15
SVSim.Database/Models/Config/RotationConfig.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Time-varying season/rotation state, populated by GlobalsImporter from prod captures.
|
||||
/// </summary>
|
||||
[Owned]
|
||||
public class RotationConfig
|
||||
{
|
||||
public string TsRotationId { get; set; } = "";
|
||||
public bool IsBattlePassPeriod { get; set; }
|
||||
public bool IsBeginnerMission { get; set; }
|
||||
public int CardSetIdForResourceDlView { get; set; }
|
||||
}
|
||||
23
SVSim.Database/Models/Config/SlotRarityWeights.cs
Normal file
23
SVSim.Database/Models/Config/SlotRarityWeights.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SVSim.Database.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Per-rarity weights for a single pack slot. Sum should be ≤ 1.0;
|
||||
/// remainder absorbs into Bronze via the PickRarity catch-all band.
|
||||
/// <para>
|
||||
/// <see cref="Slot"/> is the 1-based slot index as a string (e.g. "8") and is used as the
|
||||
/// lookup key in <see cref="PackRateConfig.PerSlot"/>. It is empty/null for the global
|
||||
/// <see cref="PackRateConfig.Default"/> entry, which has no slot affiliation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Owned]
|
||||
public class SlotRarityWeights
|
||||
{
|
||||
/// <summary>1-based slot index (as a string) for entries in PerSlot. Null/empty for the Default entry.</summary>
|
||||
public string? Slot { get; set; }
|
||||
public double Bronze { get; set; }
|
||||
public double Silver { get; set; }
|
||||
public double Gold { get; set; }
|
||||
public double Legendary { get; set; }
|
||||
}
|
||||
20
SVSim.Database/Models/GameConfigRoot.cs
Normal file
20
SVSim.Database/Models/GameConfigRoot.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database.Models.Config;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The root of <see cref="GameConfiguration.Config"/>, stored as a single jsonb column.
|
||||
/// Each sub-object defaults to its own initialiser, so `new GameConfigRoot()` is fully populated
|
||||
/// with the canonical SV Classic / DCGEngine defaults.
|
||||
/// </summary>
|
||||
[Owned]
|
||||
public class GameConfigRoot
|
||||
{
|
||||
public DefaultGrantsConfig DefaultGrants { get; set; } = new();
|
||||
public PlayerConfig Player { get; set; } = new();
|
||||
public DefaultLoadoutConfig DefaultLoadout { get; set; } = new();
|
||||
public ChallengeConfig Challenge { get; set; } = new();
|
||||
public RotationConfig Rotation { get; set; } = new();
|
||||
public PackRateConfig PackRates { get; set; } = new();
|
||||
}
|
||||
@@ -2,56 +2,13 @@ using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Server-wide tunable config and captured-from-prod state. Singleton (Id = "default") with
|
||||
/// all data living in a single typed jsonb column. See <see cref="GameConfigRoot"/> for the
|
||||
/// schema. Pre-refactor this entity had ~14 flat columns plus 4 FK navs — see migration
|
||||
/// `RefactorGameConfigurationToJsonb` for the cutover.
|
||||
/// </summary>
|
||||
public class GameConfiguration : BaseEntity<string>
|
||||
{
|
||||
public ulong DefaultCrystals { get; set; }
|
||||
|
||||
public ulong DefaultRupees { get; set; }
|
||||
|
||||
public ulong DefaultEther { get; set; }
|
||||
|
||||
public int MaxFriends { get; set; }
|
||||
|
||||
#region Time-varying globals populated by SVSim.Bootstrap.GlobalsImporter
|
||||
|
||||
/// <summary>Current "Take Two Special" rotation ID, e.g. "10015". Points into MyRotationSettingEntry.</summary>
|
||||
public string TsRotationId { get; set; } = string.Empty;
|
||||
|
||||
public bool ChallengeUseTwoPickPremiumCard { get; set; }
|
||||
|
||||
public long ChallengeTwoPickSleeveId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bool on the wire (prod sends true/false); local previously sent int. Fixes the
|
||||
/// type-mismatch noted in seed-data-strategy-2026-05-23.md crash audit.
|
||||
/// </summary>
|
||||
public bool IsBattlePassPeriod { get; set; }
|
||||
|
||||
public bool IsBeginnerMission { get; set; }
|
||||
|
||||
public int CardSetIdForResourceDlView { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Foreign Keys
|
||||
|
||||
public int DefaultDegreeId { get; set; }
|
||||
|
||||
public int DefaultEmblemId { get; set; }
|
||||
|
||||
public int DefaultMyPageBackgroundId { get; set; }
|
||||
|
||||
public int DefaultSleeveId { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Navigation Properties
|
||||
|
||||
public DegreeEntry DefaultDegree { get; set; } = new DegreeEntry();
|
||||
public EmblemEntry DefaultEmblem { get; set; } = new EmblemEntry();
|
||||
public MyPageBackgroundEntry DefaultMyPageBackground { get; set; } = new MyPageBackgroundEntry();
|
||||
public SleeveEntry DefaultSleeve { get; set; } = new SleeveEntry();
|
||||
|
||||
#endregion
|
||||
|
||||
public GameConfigRoot Config { get; set; } = new();
|
||||
}
|
||||
@@ -32,6 +32,14 @@ public class ShadowverseCardEntry : BaseEntity<long>
|
||||
/// </summary>
|
||||
public Rarity Rarity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True for foil/animated card rows (cards.json `is_foil=1`). Foils live in the same
|
||||
/// CardSet as their non-foil twin (twin's card_id = this.Id - 1). Excluded from pack
|
||||
/// draw pools by DbCardPoolProvider; reached via the per-card animated-upgrade roll
|
||||
/// in PackOpenService.
|
||||
/// </summary>
|
||||
public bool IsFoil { get; set; }
|
||||
|
||||
#region Owned
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,8 +24,8 @@ public class GlobalsRepository : IGlobalsRepository
|
||||
|
||||
public async Task<GameConfiguration> GetGameConfiguration(string key)
|
||||
{
|
||||
return await _dbContext.Set<GameConfiguration>().Include(gc => gc.DefaultMyPageBackground)
|
||||
.Include(gc => gc.DefaultEmblem).Include(gc => gc.DefaultDegree).Include(gc => gc.DefaultSleeve).FirstOrDefaultAsync(gc => gc.Id == key) ??
|
||||
// TODO: fixed in Tasks 6-7 — Include() calls removed because nav props dropped in RefactorGameConfigurationToJsonb
|
||||
return await _dbContext.Set<GameConfiguration>().FirstOrDefaultAsync(gc => gc.Id == key) ??
|
||||
new GameConfiguration();
|
||||
}
|
||||
|
||||
|
||||
@@ -81,12 +81,13 @@ public class ViewerRepository : IViewerRepository
|
||||
AccountType = socialType
|
||||
});
|
||||
|
||||
viewer.Info.MaxFriends = gameConfig.MaxFriends;
|
||||
// TODO: fixed in Task 7 — reads via Config tree after RefactorGameConfigurationToJsonb
|
||||
viewer.Info.MaxFriends = gameConfig.Config.Player.MaxFriends;
|
||||
viewer.Info.CountryCode = "KOR";
|
||||
viewer.Info.BirthDate = DateTime.UtcNow;
|
||||
viewer.Currency.Crystals = gameConfig.DefaultCrystals;
|
||||
viewer.Currency.Rupees = gameConfig.DefaultRupees;
|
||||
viewer.Currency.RedEther = gameConfig.DefaultEther;
|
||||
viewer.Currency.Crystals = gameConfig.Config.DefaultGrants.Crystals;
|
||||
viewer.Currency.Rupees = gameConfig.Config.DefaultGrants.Rupees;
|
||||
viewer.Currency.RedEther = gameConfig.Config.DefaultGrants.Ether;
|
||||
viewer.MissionData.TutorialState = 100; // finishes tutorial for now
|
||||
|
||||
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
|
||||
@@ -107,10 +108,19 @@ public class ViewerRepository : IViewerRepository
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
if (gameConfig.DefaultSleeve is not null) viewer.Sleeves.Add(gameConfig.DefaultSleeve);
|
||||
if (gameConfig.DefaultDegree is not null) viewer.Degrees.Add(gameConfig.DefaultDegree);
|
||||
if (gameConfig.DefaultEmblem is not null) viewer.Emblems.Add(gameConfig.DefaultEmblem);
|
||||
if (gameConfig.DefaultMyPageBackground is not null) viewer.MyPageBackgrounds.Add(gameConfig.DefaultMyPageBackground);
|
||||
// TODO: fixed in Task 7 — load cosmetics by ID from Config.DefaultLoadout after RefactorGameConfigurationToJsonb
|
||||
var defaultSleeveId = gameConfig.Config.DefaultLoadout.SleeveId;
|
||||
var defaultDegreeId = gameConfig.Config.DefaultLoadout.DegreeId;
|
||||
var defaultEmblemId = gameConfig.Config.DefaultLoadout.EmblemId;
|
||||
var defaultBgId = gameConfig.Config.DefaultLoadout.MyPageBackgroundId;
|
||||
var defaultSleeve = await _dbContext.Set<SleeveEntry>().FindAsync(defaultSleeveId);
|
||||
var defaultDegree = await _dbContext.Set<DegreeEntry>().FindAsync(defaultDegreeId);
|
||||
var defaultEmblem = await _dbContext.Set<EmblemEntry>().FindAsync(defaultEmblemId);
|
||||
var defaultBg = await _dbContext.Set<MyPageBackgroundEntry>().FindAsync(defaultBgId);
|
||||
if (defaultSleeve is not null) viewer.Sleeves.Add(defaultSleeve);
|
||||
if (defaultDegree is not null) viewer.Degrees.Add(defaultDegree);
|
||||
if (defaultEmblem is not null) viewer.Emblems.Add(defaultEmblem);
|
||||
if (defaultBg is not null) viewer.MyPageBackgrounds.Add(defaultBg);
|
||||
|
||||
// Grant one of each class's default leader skin. Filter out the synthetic placeholders
|
||||
// (Id=0) and dedupe — skins are many-to-many via SleeveEntryViewer-style join.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.Database.Common;
|
||||
using SVSim.Database.DataSeeders;
|
||||
@@ -112,6 +115,55 @@ public class SVSimDbContext : DbContext
|
||||
modelBuilder.Entity<PackConfigEntry>().OwnsMany(p => p.Banners);
|
||||
modelBuilder.Entity<Viewer>().OwnsMany(v => v.PackOpenCounts);
|
||||
|
||||
// GameConfiguration.Config: on Postgres use EF Core 8's OwnsOne+ToJson(jsonb column).
|
||||
// On SQLite (tests) ToJson's WriteJson has a known NullReferenceException when owned
|
||||
// collections are present — use a plain TEXT value converter instead so the same
|
||||
// entity shape works for both providers without separate test models.
|
||||
bool isSqlite = Database.ProviderName?.Contains("Sqlite", StringComparison.OrdinalIgnoreCase) == true;
|
||||
if (isSqlite)
|
||||
{
|
||||
// Store as JSON text via a value converter; EF treats Config as a single column.
|
||||
var configConverter = new ValueConverter<GameConfigRoot, string>(
|
||||
model => JsonSerializer.Serialize(model, (JsonSerializerOptions?)null),
|
||||
json => JsonSerializer.Deserialize<GameConfigRoot>(json, (JsonSerializerOptions?)null)
|
||||
?? new GameConfigRoot());
|
||||
|
||||
// Deep-equality comparer: serialize both sides and compare strings so that
|
||||
// mutations to nested properties (e.g. Config.Rotation.TsRotationId = "10015")
|
||||
// are detected by EF's snapshot change tracker and written to the DB on SaveChanges.
|
||||
var configComparer = new ValueComparer<GameConfigRoot>(
|
||||
(a, b) => JsonSerializer.Serialize(a, (JsonSerializerOptions?)null)
|
||||
== JsonSerializer.Serialize(b, (JsonSerializerOptions?)null),
|
||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null).GetHashCode(),
|
||||
v => JsonSerializer.Deserialize<GameConfigRoot>(
|
||||
JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
|
||||
(JsonSerializerOptions?)null) ?? new GameConfigRoot());
|
||||
|
||||
modelBuilder.Entity<GameConfiguration>()
|
||||
.Property(c => c.Config)
|
||||
.HasColumnName("Config")
|
||||
.HasConversion(configConverter, configComparer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Production path: jsonb column with full OwnsOne tree so EF can filter/project
|
||||
// into sub-properties at the DB level if needed.
|
||||
modelBuilder.Entity<GameConfiguration>().OwnsOne(c => c.Config, b =>
|
||||
{
|
||||
b.ToJson("Config");
|
||||
b.OwnsOne(r => r.DefaultGrants);
|
||||
b.OwnsOne(r => r.Player);
|
||||
b.OwnsOne(r => r.DefaultLoadout);
|
||||
b.OwnsOne(r => r.Challenge);
|
||||
b.OwnsOne(r => r.Rotation);
|
||||
b.OwnsOne(r => r.PackRates, pr =>
|
||||
{
|
||||
pr.OwnsOne(p => p.Default);
|
||||
pr.OwnsMany(p => p.PerSlot);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
new BaseDataSeeder().Seed(modelBuilder);
|
||||
new DefaultSettingsSeeder().Seed(modelBuilder);
|
||||
|
||||
@@ -135,4 +187,22 @@ public class SVSimDbContext : DbContext
|
||||
Database.Migrate();
|
||||
_logger.LogInformation("Migrations applied.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent runtime seed for entities that can't use HasData (notably GameConfiguration
|
||||
/// because of EF Core 8's HasData+OwnsOne(ToJson) jsonb limitation).
|
||||
/// </summary>
|
||||
public async Task EnsureSeedDataAsync()
|
||||
{
|
||||
if (!await GameConfigurations.AnyAsync(c => c.Id == "default"))
|
||||
{
|
||||
GameConfigurations.Add(new Models.GameConfiguration
|
||||
{
|
||||
Id = "default",
|
||||
Config = new Models.GameConfigRoot(),
|
||||
});
|
||||
await SaveChangesAsync();
|
||||
_logger.LogInformation("Seeded default GameConfiguration row.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user