Seeding reorg

This commit is contained in:
gamer147
2026-05-24 21:13:15 -04:00
parent 34bcc579a5
commit c14408ba06
73 changed files with 4611 additions and 369716 deletions

View File

@@ -1,8 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Common;
public interface IDataSeeder
{
void Seed(ModelBuilder builder);
}

View File

@@ -1,174 +0,0 @@
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using SVSim.Database.Common;
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Models;
namespace SVSim.Database.DataSeeders;
/// <summary>
/// Loads base data aside from cards into the DB. Cards excluded due to how many of them there are.
/// </summary>
public class BaseDataSeeder : IDataSeeder
{
private static string DataPath(string fileName) =>
Path.Combine(AppContext.BaseDirectory, "Data", fileName);
private static List<T> ReadCsv<T, TMap>(string fileName) where TMap : ClassMap<T>, new()
{
using StreamReader reader = new(DataPath(fileName));
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<TMap>();
return csv.GetRecords<T>().ToList();
}
private class ClassEntryMap : ClassMap<ClassEntry>
{
public ClassEntryMap()
{
Map(m => m.Id).Name("id");
Map(m => m.Name).Name("name");
Map(m => m.DefaultLeaderSkin).Ignore();
}
}
private class LeaderSkinEntryMap : ClassMap<LeaderSkinEntry>
{
public LeaderSkinEntryMap()
{
Map(m => m.Id).Name("class_chara_id");
Map(m => m.Name).Name("class_chara_name");
Map(m => m.ClassId).Name("clan");
Map(m => m.Class).Ignore();
Map(m => m.Viewers).Ignore();
Map(m => m.EmoteId).Ignore();
}
}
private class EmblemEntryMap : ClassMap<EmblemEntry>
{
public EmblemEntryMap()
{
Map(m => m.Id).Name("emblem_id");
}
}
private class SleeveEntryMap : ClassMap<SleeveEntry>
{
public SleeveEntryMap()
{
Map(m => m.Id).Name("sleeve_id");
}
}
private class DegreeEntryMap : ClassMap<DegreeEntry>
{
public DegreeEntryMap()
{
Map(m => m.Id).Name("degree_id");
}
}
private class BattlefieldEntryMap : ClassMap<BattlefieldEntry>
{
public BattlefieldEntryMap()
{
Map(m => m.Id).Name("value");
Map(m => m.IsOpen).Name("is_open");
}
}
private class MyPageBackgroundEntryMap : ClassMap<MyPageBackgroundEntry>
{
public MyPageBackgroundEntryMap()
{
Map(m => m.Id).Name("id");
}
}
private class ClassExpEntryMap : ClassMap<ClassExpEntry>
{
public ClassExpEntryMap()
{
Map(m => m.Id).Name("level");
Map(m => m.NecessaryExp).Name("necessary_exp");
}
}
private class RankInfoEntryMap : ClassMap<RankInfoEntry>
{
public RankInfoEntryMap()
{
Map(m => m.Id).Name("rank_id");
Map(m => m.Name).Name("rank_name");
Map(m => m.NecessaryPoint).Name("necessary_point");
Map(m => m.AccumulatePoint).Name("accumulate_point");
Map(m => m.LowerLimitPoint).Name("lower_limit_point");
Map(m => m.BaseAddBp).Name("base_add_bp");
Map(m => m.BaseDropBp).Name("base_drop_bp");
Map(m => m.StreakBonusPt).Name("streak_bonus_pt");
Map(m => m.WinBonus).Name("win_bonus");
Map(m => m.LoseBonus).Name("lose_bonus");
Map(m => m.MaxWinBonus).Name("max_win_bonus");
Map(m => m.MaxLoseBonus).Name("max_lose_bonus");
Map(m => m.IsPromotionWar).Name("is_promotion_war");
Map(m => m.MatchCount).Name("match_count");
Map(m => m.NecessaryWin).Name("necessary_win");
Map(m => m.ResetLose).Name("reset_lose");
Map(m => m.AccumulateMasterPoint).Name("accumulate_master_point");
}
}
private class CardCosmeticRewardMap : ClassMap<CardCosmeticReward>
{
public CardCosmeticRewardMap()
{
Map(m => m.CardId).Name("card_id");
Map(m => m.Type).Name("type");
Map(m => m.CosmeticId).Name("cosmetic_id");
Map(m => m.Quantity).Name("quantity").Default(1);
Map(m => m.Card).Ignore();
}
}
public void Seed(ModelBuilder builder)
{
// Migrations bake the HasData rows into InsertData calls — once the migration is
// generated, runtime model-creation no longer needs the CSVs. Tools that only query
// an already-migrated DB (e.g. SVSim.CardImport) don't ship the Data folder; skip
// gracefully so DbContext construction succeeds for them.
if (!File.Exists(DataPath("classes.csv")))
{
Console.Error.WriteLine($"[BaseDataSeeder] Skipping seed: Data folder not found at {DataPath("")}");
return;
}
List<ClassEntry> classes = ReadCsv<ClassEntry, ClassEntryMap>("classes.csv");
List<LeaderSkinEntry> leaderSkins = ReadCsv<LeaderSkinEntry, LeaderSkinEntryMap>("leaderskins.csv");
leaderSkins.ForEach(skin =>
{
if (skin.ClassId == 0)
{
skin.ClassId = null;
}
});
List<EmblemEntry> emblems = ReadCsv<EmblemEntry, EmblemEntryMap>("emblems.csv");
List<DegreeEntry> degrees = ReadCsv<DegreeEntry, DegreeEntryMap>("degrees.csv");
List<SleeveEntry> sleeves = ReadCsv<SleeveEntry, SleeveEntryMap>("sleeves.csv");
List<BattlefieldEntry> battlefields = ReadCsv<BattlefieldEntry, BattlefieldEntryMap>("battlefields.csv");
List<MyPageBackgroundEntry> myPageBackgrounds = ReadCsv<MyPageBackgroundEntry, MyPageBackgroundEntryMap>("mypagebackgrounds.csv");
List<RankInfoEntry> rankinfos = ReadCsv<RankInfoEntry, RankInfoEntryMap>("ranks.csv");
List<ClassExpEntry> classexp = ReadCsv<ClassExpEntry, ClassExpEntryMap>("classexp.csv");
List<CardCosmeticReward> cardCosmeticRewards = ReadCsv<CardCosmeticReward, CardCosmeticRewardMap>("card_cosmetic_rewards.csv");
builder.Entity<ClassEntry>().HasData(classes);
builder.Entity<LeaderSkinEntry>().HasData(leaderSkins);
builder.Entity<EmblemEntry>().HasData(emblems);
builder.Entity<SleeveEntry>().HasData(sleeves);
builder.Entity<DegreeEntry>().HasData(degrees);
builder.Entity<BattlefieldEntry>().HasData(battlefields);
builder.Entity<MyPageBackgroundEntry>().HasData(myPageBackgrounds);
builder.Entity<RankInfoEntry>().HasData(rankinfos);
builder.Entity<ClassExpEntry>().HasData(classexp);
builder.Entity<CardCosmeticReward>().HasData(cardCosmeticRewards);
}
}

View File

@@ -1,16 +0,0 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Common;
namespace SVSim.Database.DataSeeders;
public class DefaultSettingsSeeder : IDataSeeder
{
public void Seed(ModelBuilder builder)
{
// 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).
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,489 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class ProdContentTables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
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<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<string>(
name: "TsRotationId",
table: "GameConfigurations",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.CreateTable(
name: "ArenaSeasons",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
Mode = table.Column<int>(type: "integer", nullable: false),
Enable = table.Column<int>(type: "integer", nullable: false),
Cost = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
RupyCost = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
TicketCost = table.Column<int>(type: "integer", nullable: false),
IsJoin = table.Column<bool>(type: "boolean", nullable: false),
FormatInfo = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ArenaSeasons", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AvatarAbilities",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
LeaderSkinId = table.Column<int>(type: "integer", nullable: false),
BattleStartFirstPlayerTurnBp = table.Column<int>(type: "integer", nullable: false),
BattleStartSecondPlayerTurnBp = table.Column<int>(type: "integer", nullable: false),
BattleStartMaxLife = table.Column<int>(type: "integer", nullable: false),
AbilityCost = table.Column<string>(type: "text", nullable: false),
Ability = table.Column<string>(type: "text", nullable: false),
PassiveAbility = table.Column<string>(type: "text", nullable: false),
AbilityDesc = table.Column<string>(type: "text", nullable: false),
PassiveAbilityDesc = table.Column<string>(type: "text", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AvatarAbilities", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Banners",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
ImageName = table.Column<string>(type: "text", nullable: false),
Click = table.Column<string>(type: "text", nullable: false),
Status = table.Column<string>(type: "text", nullable: false),
ChangeTime = table.Column<int>(type: "integer", nullable: false),
RemainingTime = table.Column<int>(type: "integer", nullable: false),
ImagePaths = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Banners", x => x.Id);
});
migrationBuilder.CreateTable(
name: "BattlePassLevels",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
Level = table.Column<int>(type: "integer", nullable: false),
RewardData = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BattlePassLevels", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Colosseums",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
ColosseumId = table.Column<string>(type: "text", nullable: false),
ColosseumName = table.Column<string>(type: "text", nullable: false),
CardPoolName = table.Column<string>(type: "text", nullable: false),
DeckFormat = table.Column<string>(type: "text", nullable: false),
StartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
NowRound = table.Column<string>(type: "text", nullable: false),
IsDisplayTips = table.Column<string>(type: "text", nullable: false),
TipsId = table.Column<string>(type: "text", nullable: false),
IsColosseumPeriod = table.Column<bool>(type: "boolean", nullable: false),
IsRoundPeriod = table.Column<bool>(type: "boolean", nullable: false),
IsNormalTwoPick = table.Column<string>(type: "text", nullable: false),
IsSpecialMode = table.Column<string>(type: "text", nullable: false),
IsAllCardEnabled = table.Column<int>(type: "integer", nullable: false),
SalesPeriodInfo = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Colosseums", x => x.Id);
});
migrationBuilder.CreateTable(
name: "DailyLoginBonuses",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
BonusId = table.Column<int>(type: "integer", nullable: false),
BonusData = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DailyLoginBonuses", x => x.Id);
});
migrationBuilder.CreateTable(
name: "DefaultDecks",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
DeckNo = table.Column<int>(type: "integer", nullable: false),
ClassId = table.Column<int>(type: "integer", nullable: false),
SleeveId = table.Column<long>(type: "bigint", nullable: false),
LeaderSkinId = table.Column<int>(type: "integer", nullable: false),
DeckName = table.Column<string>(type: "text", nullable: false),
CardIdArray = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DefaultDecks", x => x.Id);
});
migrationBuilder.CreateTable(
name: "DefaultLeaderSkinSettings",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
ClassId = table.Column<int>(type: "integer", nullable: false),
IsRandomLeaderSkin = table.Column<int>(type: "integer", nullable: false),
LeaderSkinId = table.Column<int>(type: "integer", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DefaultLeaderSkinSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "FeatureMaintenances",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
FeatureKey = table.Column<string>(type: "text", nullable: false),
Data = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FeatureMaintenances", x => x.Id);
});
migrationBuilder.CreateTable(
name: "LoadingExclusionCards",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false),
CardId = table.Column<long>(type: "bigint", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LoadingExclusionCards", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MaintenanceCards",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false),
CardId = table.Column<long>(type: "bigint", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MaintenanceCards", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MasterPointRankingPeriods",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
PeriodNum = table.Column<int>(type: "integer", nullable: false),
NecessaryScore = table.Column<long>(type: "bigint", nullable: false),
BeginTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MasterPointRankingPeriods", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MyRotationAbilities",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
AbilityId = table.Column<int>(type: "integer", nullable: false),
Data = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MyRotationAbilities", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MyRotationSettings",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
RotationId = table.Column<int>(type: "integer", nullable: false),
CardSetIdsCsv = table.Column<string>(type: "text", nullable: false),
AbilitiesCsv = table.Column<string>(type: "text", nullable: false),
ReprintedCardIds = table.Column<string>(type: "jsonb", nullable: false),
RestrictedCardIds = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MyRotationSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PreReleaseInfos",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
PreReleaseId = table.Column<string>(type: "text", nullable: false),
NextCardSetId = table.Column<string>(type: "text", nullable: false),
StartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DisplayEndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
FreeMatchStartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CardMasterId = table.Column<int>(type: "integer", nullable: false),
DefaultCardMasterId = table.Column<string>(type: "text", nullable: false),
PreReleaseCardMasterId = table.Column<string>(type: "text", nullable: false),
IsPreRotationFreeMatchTerm = table.Column<bool>(type: "boolean", nullable: false),
RotationCardSetIdList = table.Column<string>(type: "jsonb", nullable: false),
ReprintedBaseCardIds = table.Column<string>(type: "jsonb", nullable: false),
LatestReprintedBaseCardIds = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PreReleaseInfos", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ReprintedCards",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false),
CardId = table.Column<long>(type: "bigint", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ReprintedCards", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SealedSeasons",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
Enable = table.Column<int>(type: "integer", nullable: false),
CrystalCost = table.Column<int>(type: "integer", nullable: false),
RupyCost = table.Column<int>(type: "integer", nullable: false),
TicketCost = table.Column<int>(type: "integer", nullable: false),
DeckUsingNumMin = table.Column<int>(type: "integer", nullable: false),
ScheduleId = table.Column<int>(type: "integer", nullable: false),
IsJoin = table.Column<bool>(type: "boolean", nullable: false),
IsDeckCodeMaintenance = table.Column<bool>(type: "boolean", nullable: false),
PackInfo = table.Column<string>(type: "jsonb", nullable: false),
SalesPeriodInfo = table.Column<string>(type: "jsonb", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SealedSeasons", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SpotCards",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false),
CardId = table.Column<long>(type: "bigint", nullable: false),
Cost = table.Column<int>(type: "integer", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SpotCards", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UnlimitedRestrictions",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false),
CardId = table.Column<long>(type: "bigint", nullable: false),
RestrictionValue = table.Column<int>(type: "integer", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UnlimitedRestrictions", x => x.Id);
});
migrationBuilder.UpdateData(
table: "GameConfigurations",
keyColumn: "Id",
keyValue: "default",
columns: new[] { "CardSetIdForResourceDlView", "ChallengeTwoPickSleeveId", "ChallengeUseTwoPickPremiumCard", "IsBattlePassPeriod", "IsBeginnerMission", "TsRotationId" },
values: new object[] { 0, 0L, false, false, false, "" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ArenaSeasons");
migrationBuilder.DropTable(
name: "AvatarAbilities");
migrationBuilder.DropTable(
name: "Banners");
migrationBuilder.DropTable(
name: "BattlePassLevels");
migrationBuilder.DropTable(
name: "Colosseums");
migrationBuilder.DropTable(
name: "DailyLoginBonuses");
migrationBuilder.DropTable(
name: "DefaultDecks");
migrationBuilder.DropTable(
name: "DefaultLeaderSkinSettings");
migrationBuilder.DropTable(
name: "FeatureMaintenances");
migrationBuilder.DropTable(
name: "LoadingExclusionCards");
migrationBuilder.DropTable(
name: "MaintenanceCards");
migrationBuilder.DropTable(
name: "MasterPointRankingPeriods");
migrationBuilder.DropTable(
name: "MyRotationAbilities");
migrationBuilder.DropTable(
name: "MyRotationSettings");
migrationBuilder.DropTable(
name: "PreReleaseInfos");
migrationBuilder.DropTable(
name: "ReprintedCards");
migrationBuilder.DropTable(
name: "SealedSeasons");
migrationBuilder.DropTable(
name: "SpotCards");
migrationBuilder.DropTable(
name: "UnlimitedRestrictions");
migrationBuilder.DropColumn(
name: "CardSetIdForResourceDlView",
table: "GameConfigurations");
migrationBuilder.DropColumn(
name: "ChallengeTwoPickSleeveId",
table: "GameConfigurations");
migrationBuilder.DropColumn(
name: "ChallengeUseTwoPickPremiumCard",
table: "GameConfigurations");
migrationBuilder.DropColumn(
name: "IsBattlePassPeriod",
table: "GameConfigurations");
migrationBuilder.DropColumn(
name: "IsBeginnerMission",
table: "GameConfigurations");
migrationBuilder.DropColumn(
name: "TsRotationId",
table: "GameConfigurations");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class MypageRoomTypeInSession : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SpecialDeckFormats",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
DeckFormat = table.Column<string>(type: "text", nullable: false),
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SpecialDeckFormats", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SpecialDeckFormats");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class MypagePaymentItems : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PaymentItems",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
ProductId = table.Column<int>(type: "integer", nullable: false),
StoreProductId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Text = table.Column<string>(type: "text", nullable: false),
Price = table.Column<decimal>(type: "numeric", nullable: false),
ChargeCrystalNum = table.Column<int>(type: "integer", nullable: false),
FreeCrystalNum = table.Column<int>(type: "integer", nullable: false),
PurchaseLimit = table.Column<int>(type: "integer", nullable: false),
SpecialShopFlag = table.Column<int>(type: "integer", nullable: false),
ImageName = table.Column<string>(type: "text", nullable: false),
StartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
RemainingTime = table.Column<int>(type: "integer", nullable: false),
IsResaleProduct = table.Column<int>(type: "integer", nullable: false),
ResaleStartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentItems", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentItems");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class PracticeOpponents : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PracticeOpponents",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
PracticeId = table.Column<int>(type: "integer", nullable: false),
TextId = table.Column<string>(type: "text", nullable: false),
ClassId = table.Column<int>(type: "integer", nullable: false),
CharaId = table.Column<int>(type: "integer", nullable: false),
DegreeId = table.Column<int>(type: "integer", nullable: false),
AiDeckLevel = table.Column<int>(type: "integer", nullable: false),
AiLogicLevel = table.Column<int>(type: "integer", nullable: false),
AiMaxLife = table.Column<int>(type: "integer", nullable: false),
Battle3dFieldId = table.Column<string>(type: "text", nullable: false),
IsMaintenance = table.Column<bool>(type: "boolean", nullable: false),
IsCampaignPractice = table.Column<bool>(type: "boolean", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PracticeOpponents", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PracticeOpponents");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,135 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class AddPackCatalog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Packs",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
BasePackId = table.Column<int>(type: "integer", nullable: false),
GachaType = table.Column<int>(type: "integer", nullable: false),
PackCategory = table.Column<int>(type: "integer", nullable: false),
PosterType = table.Column<int>(type: "integer", nullable: false),
CommenceDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CompleteDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
SalesPeriodTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
SleeveId = table.Column<int>(type: "integer", nullable: false),
SpecialSleeveId = table.Column<int>(type: "integer", nullable: false),
OverrideDrawEffectPackId = table.Column<int>(type: "integer", nullable: false),
OverrideUiEffectPackId = table.Column<int>(type: "integer", nullable: false),
GachaDetail = table.Column<string>(type: "text", nullable: false),
IsHide = table.Column<bool>(type: "boolean", nullable: false),
IsNew = table.Column<bool>(type: "boolean", nullable: false),
IsPreRelease = table.Column<bool>(type: "boolean", nullable: false),
OpenCountLimit = table.Column<int>(type: "integer", nullable: false),
GachaPointConfig_ExchangeablePoint = table.Column<int>(type: "integer", nullable: true),
GachaPointConfig_IncreaseGachaPoint = table.Column<int>(type: "integer", nullable: true),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Packs", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ViewerPackOpenCount",
columns: table => new
{
ViewerId = table.Column<long>(type: "bigint", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PackId = table.Column<int>(type: "integer", nullable: false),
OpenCount = table.Column<int>(type: "integer", nullable: false),
LastDailyFreeAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ViewerPackOpenCount", x => new { x.ViewerId, x.Id });
table.ForeignKey(
name: "FK_ViewerPackOpenCount_Viewers_ViewerId",
column: x => x.ViewerId,
principalTable: "Viewers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PackBannerEntry",
columns: table => new
{
PackConfigEntryId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
BannerName = table.Column<string>(type: "text", nullable: false),
DialogTitle = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PackBannerEntry", x => new { x.PackConfigEntryId, x.Id });
table.ForeignKey(
name: "FK_PackBannerEntry_Packs_PackConfigEntryId",
column: x => x.PackConfigEntryId,
principalTable: "Packs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PackChildGachaEntry",
columns: table => new
{
PackConfigEntryId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
GachaId = table.Column<int>(type: "integer", nullable: false),
TypeDetail = table.Column<int>(type: "integer", nullable: false),
Cost = table.Column<int>(type: "integer", nullable: false),
CardCount = table.Column<int>(type: "integer", nullable: false),
ItemId = table.Column<long>(type: "bigint", nullable: true),
IsDailySingle = table.Column<bool>(type: "boolean", nullable: false),
OverrideIncreaseGachaPoint = table.Column<int>(type: "integer", nullable: false),
PurchaseLimitCount = table.Column<int>(type: "integer", nullable: false),
FreeGachaCampaignId = table.Column<int>(type: "integer", nullable: true),
CampaignName = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PackChildGachaEntry", x => new { x.PackConfigEntryId, x.Id });
table.ForeignKey(
name: "FK_PackChildGachaEntry_Packs_PackConfigEntryId",
column: x => x.PackConfigEntryId,
principalTable: "Packs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PackBannerEntry");
migrationBuilder.DropTable(
name: "PackChildGachaEntry");
migrationBuilder.DropTable(
name: "ViewerPackOpenCount");
migrationBuilder.DropTable(
name: "Packs");
}
}
}

View File

@@ -1,338 +0,0 @@
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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +0,0 @@
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");
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models.Config;
[Owned]
[ConfigSection("Challenge")]
public class ChallengeConfig
{
public bool UseTwoPickPremiumCard { get; set; }
public long TwoPickSleeveId { get; set; }
public static ChallengeConfig ShippedDefaults() => new();
}

View File

@@ -0,0 +1,14 @@
namespace SVSim.Database.Models.Config;
/// <summary>
/// Marks a POCO as a top-level GameConfig section. The <see cref="Name"/> is the storage key —
/// it's the primary key in the <c>GameConfigs</c> table and the appsettings.json section name
/// under <c>"GameConfig"</c>. Renaming a class is safe; renaming the section name here is a
/// breaking change to stored data and config files.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ConfigSectionAttribute : Attribute
{
public string Name { get; }
public ConfigSectionAttribute(string name) => Name = name;
}

View File

@@ -1,12 +1,12 @@
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models.Config;
/// <summary>Per-viewer-registration default currency grants.</summary>
[Owned]
[ConfigSection("DefaultGrants")]
public class DefaultGrantsConfig
{
public ulong Crystals { get; set; } = 50000;
public ulong Rupees { get; set; } = 50000;
public ulong Ether { get; set; } = 50000;
public static DefaultGrantsConfig ShippedDefaults() => new();
}

View File

@@ -1,17 +1,16 @@
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)).
/// Default cosmetic loadout ids for a newly-registered viewer. Untyped longs in the jsonb tree
/// (FK validation would live in a future config-editing UI — see TODO(config-validation)).
/// </summary>
[Owned]
[ConfigSection("DefaultLoadout")]
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;
public static DefaultLoadoutConfig ShippedDefaults() => new();
}

View File

@@ -0,0 +1,36 @@
namespace SVSim.Database.Models.Config;
/// <summary>
/// Window-based schedule for the Custom Rotation (a.k.a. MyRotation) feature. Two parallel windows:
/// <c>Gathering</c> (deck-building period) and <c>FreeBattle</c> (active play period). The client
/// gates the format-selector button on these windows — see Wizard/MyRotationAllInfo.cs:45
/// (<c>IsMyRotationEnable =&gt; IsWithinPeriod(FreeMatchPeriod)</c>) and Wizard/DeckListUI.cs:92.
/// Mapped to the wire-shape <c>SpecialRotationSchedule</c> at the controller seam.
/// <para>
/// Shipped defaults reproduce the 2026-05-23 prod capture so a fresh install ships with the
/// feature enabled. GlobalsImporter overwrites the DB section from any newer capture.
/// </para>
/// </summary>
[ConfigSection("MyRotationSchedule")]
public class MyRotationScheduleConfig
{
public ScheduleWindow Gathering { get; set; } = new()
{
Begin = new DateTime(2024, 5, 1, 20, 0, 0, DateTimeKind.Utc),
End = new DateTime(2030, 6, 26, 19, 59, 59, DateTimeKind.Utc),
};
public ScheduleWindow FreeBattle { get; set; } = new()
{
Begin = new DateTime(2024, 5, 1, 20, 0, 0, DateTimeKind.Utc),
End = new DateTime(2030, 6, 26, 19, 59, 59, DateTimeKind.Utc),
};
public static MyRotationScheduleConfig ShippedDefaults() => new();
}
public class ScheduleWindow
{
public DateTime Begin { get; set; }
public DateTime End { get; set; }
}

View File

@@ -1,12 +1,11 @@
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.
/// Tunables for pack-opening RNG. Property initialisers reproduce the original Shadowverse
/// Classic main-slot rates exactly. Collection-shaped defaults (slot-8 PerSlot entry) live in
/// <see cref="ShippedDefaults"/>, not in the initialiser — see PerSlot docstring.
/// </summary>
[Owned]
[ConfigSection("PackRates")]
public class PackRateConfig
{
/// <summary>
@@ -27,21 +26,32 @@ public class PackRateConfig
};
/// <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.
/// Per-slot overrides keyed by 1-based slot index (stored as a list for json compatibility —
/// Dictionary&lt;string,T&gt; of complex owned types is not supported). Look up by
/// <see cref="SlotRarityWeights.Slot"/>. 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.
/// <para>
/// MUST default to empty. The original EF Core 8 <c>OwnsMany</c>+<c>ToJson</c> path APPENDED
/// jsonb rows onto whatever collection the parent's parameterless ctor produced — a non-empty
/// initialiser here meant every config load doubled-up and the original seed silently won the
/// <c>FirstOrDefault</c> lookup in <c>PackOpenService.ResolveWeights</c>. The EF path is gone
/// now (config goes through <c>IGameConfigService</c> + STJ), but the rule stays: collection
/// defaults live in <see cref="ShippedDefaults"/>, not in property initialisers.
/// </para>
/// </summary>
public List<SlotRarityWeights> PerSlot { get; set; } = [];
/// <summary>
/// Per-slot overrides keyed by 1-based slot index (stored as a list for EF Core 8 json
/// compatibility — Dictionary&lt;string,T&gt; 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).
/// Canonical SV Classic shipped defaults — what an operator gets if neither the DB nor
/// appsettings.json supplies a PackRates section. Source of truth for the fresh-install seeder
/// and the <c>IGameConfigService</c> inline-default tier.
/// </summary>
public List<SlotRarityWeights> PerSlot { get; set; } =
[
new() { Slot = "8", Bronze = 0, Silver = 0.7692, Gold = 0.1846, Legendary = 0.0462 },
];
public static PackRateConfig ShippedDefaults() => new()
{
PerSlot =
{
new SlotRarityWeights { Slot = "8", Bronze = 0, Silver = 0.7692, Gold = 0.1846, Legendary = 0.0462 },
},
};
}

View File

@@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models.Config;
[Owned]
[ConfigSection("Player")]
public class PlayerConfig
{
public int MaxFriends { get; set; } = 20;
public static PlayerConfig ShippedDefaults() => new();
}

View File

@@ -1,15 +1,15 @@
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models.Config;
/// <summary>
/// Time-varying season/rotation state, populated by GlobalsImporter from prod captures.
/// </summary>
[Owned]
[ConfigSection("Rotation")]
public class RotationConfig
{
public string TsRotationId { get; set; } = "";
public bool IsBattlePassPeriod { get; set; }
public bool IsBeginnerMission { get; set; }
public int CardSetIdForResourceDlView { get; set; }
public static RotationConfig ShippedDefaults() => new();
}

View File

@@ -1,5 +1,3 @@
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models.Config;
/// <summary>
@@ -7,11 +5,10 @@ namespace SVSim.Database.Models.Config;
/// 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
/// lookup key in <see cref="PackRateConfig.PerSlot"/>. It is null/empty 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>

View File

@@ -1,20 +0,0 @@
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();
}

View File

@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using SVSim.Database.Common;
namespace SVSim.Database.Models;
/// <summary>
/// One row per top-level game-config section. <see cref="SectionName"/> matches the
/// <c>ConfigSectionAttribute.Name</c> on the corresponding POCO in <c>Models.Config</c>
/// (e.g. <c>"PackRates"</c> → <c>PackRateConfig</c>). <see cref="ValueJson"/> is the section's
/// payload, stored as <c>jsonb</c> on Postgres and <c>TEXT</c> on SQLite.
/// <para>
/// Deserialisation goes through pure System.Text.Json in <c>IGameConfigService</c> — EF doesn't
/// know about the section POCOs. Replaces the old single-row <c>GameConfigurations</c> table
/// (one wide jsonb document, EF Core 8 <c>OwnsOne</c>+<c>ToJson</c> tree). See ADR-pending /
/// 2026-05-24 config-refactor discussion for the why.
/// </para>
/// </summary>
public class GameConfigSection : ITimeTrackedEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string SectionName { get; set; } = "";
/// <summary>Raw JSON payload for this section. Postgres stores as jsonb; SQLite as TEXT.</summary>
public string ValueJson { get; set; } = "{}";
public DateTime DateCreated { get; set; } = DateTime.MinValue;
public DateTime? DateUpdated { get; set; }
}

View File

@@ -1,14 +0,0 @@
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 GameConfigRoot Config { get; set; } = new();
}

View File

@@ -22,13 +22,6 @@ public class GlobalsRepository : IGlobalsRepository
return await _dbContext.Set<BattlefieldEntry>().Where(bf => !onlyOpen || bf.IsOpen).ToListAsync();
}
public async Task<GameConfiguration> GetGameConfiguration(string 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();
}
public async Task<List<RankInfoEntry>> GetRankInfo()
{
return await _dbContext.Set<RankInfoEntry>().ToListAsync();

View File

@@ -6,7 +6,6 @@ public interface IGlobalsRepository
{
Task<List<ClassExpEntry>> GetClassExpCurve();
Task<List<BattlefieldEntry>> GetBattlefields(bool onlyOpen);
Task<GameConfiguration> GetGameConfiguration(string key);
Task<List<RankInfoEntry>> GetRankInfo();
// Prod-captured globals — populated by SVSim.Bootstrap.GlobalsImporter.

View File

@@ -1,20 +1,22 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Card;
using SVSim.Database.Repositories.Globals;
using SVSim.Database.Models.Config;
using SVSim.Database.Services;
namespace SVSim.Database.Repositories.Viewer;
public class ViewerRepository : IViewerRepository
{
protected readonly SVSimDbContext _dbContext;
private readonly IGameConfigService _config;
private const int MaxFriends = 20;
public ViewerRepository(SVSimDbContext dbContext)
public ViewerRepository(SVSimDbContext dbContext, IGameConfigService config)
{
_dbContext = dbContext;
_config = config;
}
public async Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId)
@@ -73,7 +75,9 @@ public class ViewerRepository : IViewerRepository
{
DisplayName = displayName
};
GameConfiguration gameConfig = await new GlobalsRepository(_dbContext).GetGameConfiguration("default");
var player = _config.Get<PlayerConfig>();
var grants = _config.Get<DefaultGrantsConfig>();
var loadout = _config.Get<DefaultLoadoutConfig>();
viewer.SocialAccountConnections.Add(new SocialAccountConnection
{
@@ -81,13 +85,12 @@ public class ViewerRepository : IViewerRepository
AccountType = socialType
});
// TODO: fixed in Task 7 — reads via Config tree after RefactorGameConfigurationToJsonb
viewer.Info.MaxFriends = gameConfig.Config.Player.MaxFriends;
viewer.Info.MaxFriends = player.MaxFriends;
viewer.Info.CountryCode = "KOR";
viewer.Info.BirthDate = DateTime.UtcNow;
viewer.Currency.Crystals = gameConfig.Config.DefaultGrants.Crystals;
viewer.Currency.Rupees = gameConfig.Config.DefaultGrants.Rupees;
viewer.Currency.RedEther = gameConfig.Config.DefaultGrants.Ether;
viewer.Currency.Crystals = grants.Crystals;
viewer.Currency.Rupees = grants.Rupees;
viewer.Currency.RedEther = grants.Ether;
viewer.MissionData.TutorialState = 100; // finishes tutorial for now
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
@@ -108,11 +111,10 @@ public class ViewerRepository : IViewerRepository
};
}).ToList();
// 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 defaultSleeveId = loadout.SleeveId;
var defaultDegreeId = loadout.DegreeId;
var defaultEmblemId = loadout.EmblemId;
var defaultBgId = loadout.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);

View File

@@ -1,11 +1,8 @@
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;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
namespace SVSim.Database;
@@ -38,7 +35,7 @@ public class SVSimDbContext : DbContext
public DbSet<RankInfoEntry> RankInfo => Set<RankInfoEntry>();
public DbSet<ItemEntry> Items => Set<ItemEntry>();
public DbSet<GameConfiguration> GameConfigurations => Set<GameConfiguration>();
public DbSet<GameConfigSection> GameConfigs => Set<GameConfigSection>();
// Prod-captured globals — populated by SVSim.Bootstrap, not HasData. See
// docs/audits/prod-data-capture-strategy-2026-05-23.md.
@@ -128,57 +125,18 @@ public class SVSimDbContext : DbContext
.OnDelete(DeleteBehavior.Cascade);
});
// 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)
// GameConfigSection: one row per top-level config section. Postgres stores ValueJson as
// jsonb (gives jsonb-side queryability if needed later); SQLite gets a plain TEXT column.
// EF never sees the section POCO shapes — IGameConfigService owns deserialisation via STJ.
// Replaces the old single-row GameConfigurations table with its EF Core 8 OwnsOne+ToJson
// tree; see 2026-05-24 config refactor.
bool isPostgres = Database.ProviderName?.Contains("Npgsql", StringComparison.OrdinalIgnoreCase) == true;
if (isPostgres)
{
// 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);
modelBuilder.Entity<GameConfigSection>()
.Property(s => s.ValueJson)
.HasColumnType("jsonb");
}
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);
base.OnModelCreating(modelBuilder);
}
@@ -202,20 +160,60 @@ public class SVSimDbContext : DbContext
}
/// <summary>
/// Idempotent runtime seed for entities that can't use HasData (notably GameConfiguration
/// because of EF Core 8's HasData+OwnsOne(ToJson) jsonb limitation).
/// Idempotent runtime seed for entities that can't use HasData. For GameConfigSection: walks
/// every <see cref="ConfigSectionAttribute"/>-marked POCO in the Models.Config namespace and
/// inserts a row containing its <c>ShippedDefaults()</c> payload if no row for that section
/// name exists. Safe to run on every startup — only missing rows are added; operator-edited
/// rows are left alone.
/// </summary>
public async Task EnsureSeedDataAsync()
{
if (!await GameConfigurations.AnyAsync(c => c.Id == "default"))
var existing = await GameConfigs.Select(s => s.SectionName).ToListAsync();
var existingSet = new HashSet<string>(existing, StringComparer.Ordinal);
int added = 0;
foreach (var (name, json) in EnumerateShippedDefaults())
{
if (existingSet.Contains(name)) continue;
GameConfigs.Add(new GameConfigSection { SectionName = name, ValueJson = json });
added++;
}
if (added > 0)
{
GameConfigurations.Add(new Models.GameConfiguration
{
Id = "default",
Config = new Models.GameConfigRoot(),
});
await SaveChangesAsync();
_logger.LogInformation("Seeded default GameConfiguration row.");
_logger.LogInformation("Seeded {Count} default GameConfigSection row(s).", added);
}
}
private static IEnumerable<(string Name, string Json)> EnumerateShippedDefaults()
{
// Reflect over every [ConfigSection]-marked type in the same assembly as PackRateConfig.
// Each type must expose a parameterless `public static T ShippedDefaults()` — see the
// POCOs in Models/Config for the convention.
var asm = typeof(PackRateConfig).Assembly;
var stjOptions = new System.Text.Json.JsonSerializerOptions
{
WriteIndented = false,
};
foreach (var t in asm.GetTypes())
{
var attr = t.GetCustomAttributes(typeof(ConfigSectionAttribute), inherit: false)
.Cast<ConfigSectionAttribute>().FirstOrDefault();
if (attr is null) continue;
var factory = t.GetMethod("ShippedDefaults",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static,
binder: null, types: Type.EmptyTypes, modifiers: null);
if (factory is null)
{
throw new InvalidOperationException(
$"[ConfigSection] type {t.FullName} is missing `public static {t.Name} ShippedDefaults()`.");
}
var instance = factory.Invoke(null, null)
?? throw new InvalidOperationException($"{t.FullName}.ShippedDefaults() returned null.");
yield return (attr.Name, System.Text.Json.JsonSerializer.Serialize(instance, t, stjOptions));
}
}
}

View File

@@ -0,0 +1,21 @@
namespace SVSim.Database.Services;
/// <summary>
/// Read-only access to game-domain configuration. Resolves each section atomically through the
/// tier chain: DB row in <c>GameConfigs</c> → <c>appsettings.json</c> section
/// <c>"GameConfig:&lt;SectionName&gt;"</c> → <c>T.ShippedDefaults()</c> → <c>new T()</c>.
/// <para>
/// "Atomic" means: the first tier that has the section wins entirely; tiers are not merged
/// per-property. This is deliberate — see 2026-05-24 config refactor discussion. Caching is
/// not implemented today (scoped lifetime; one DB read per request); the interface is shaped
/// to allow it to be added later without changing call sites.
/// </para>
/// </summary>
public interface IGameConfigService
{
/// <summary>
/// Resolves the section identified by <typeparamref name="T"/>'s
/// <c>ConfigSectionAttribute</c>. Throws if the type is not annotated.
/// </summary>
T Get<T>() where T : class, new();
}