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

@@ -0,0 +1,78 @@
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Reads <c>card_cosmetic_rewards.csv</c> and upserts <see cref="CardCosmeticReward"/> rows.
/// MUST run after <see cref="CardImporter"/> — the table has an FK to <c>Cards.Id</c>, so any
/// reward whose CardId isn't in the freshly-imported cards table is skipped with a warning.
/// </summary>
public class CardCosmeticRewardImporter
{
public async Task ImportAsync(SVSimDbContext context, string dataDir)
{
string path = Path.Combine(dataDir, "card_cosmetic_rewards.csv");
if (!File.Exists(path))
{
Console.Error.WriteLine($"[CardCosmeticRewardImporter] Missing CSV: {path}");
return;
}
Console.WriteLine($"[CardCosmeticRewardImporter] Reading {path}...");
List<CardCosmeticReward> rows;
using (var reader = new StreamReader(path))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Context.RegisterClassMap<CardCosmeticRewardMap>();
rows = csv.GetRecords<CardCosmeticReward>().ToList();
}
var validCardIds = (await context.Cards.Select(c => c.Id).ToListAsync()).ToHashSet();
var existing = (await context.CardCosmeticRewards.ToListAsync())
.ToDictionary(r => (r.CardId, r.Type, r.CosmeticId));
int created = 0, updated = 0, skipped = 0;
foreach (var r in rows)
{
if (!validCardIds.Contains(r.CardId))
{
skipped++;
continue;
}
var key = (r.CardId, r.Type, r.CosmeticId);
if (existing.TryGetValue(key, out var e))
{
if (e.Quantity != r.Quantity) { e.Quantity = r.Quantity; updated++; }
}
else
{
context.CardCosmeticRewards.Add(r);
created++;
}
}
await context.SaveChangesAsync();
Console.WriteLine(
$"[CardCosmeticRewardImporter] Done: +{created} / ~{updated}, " +
$"skipped {skipped} (no matching card row).");
}
private sealed 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();
}
}
}

View File

@@ -9,8 +9,7 @@ namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Reads the loader's card dump (LitJson array of CardCSVData) and upserts ShadowverseCardEntry +
/// ShadowverseCardSetEntry rows. Lifted unchanged from the original SVSim.CardImport.Program.Main —
/// only the orchestration was moved into <see cref="Program"/>.
/// ShadowverseCardSetEntry rows. Idempotent.
/// </summary>
public class CardImporter
{

View File

@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
using static SVSim.Bootstrap.Importers.ImporterBase;
namespace SVSim.Bootstrap.Importers;
@@ -87,32 +88,95 @@ public class GlobalsImporter
return total;
}
// ---------- GameConfiguration ----------
// ---------- GameConfig sections ----------
private async Task<int> ImportGameConfigurationExtensions(SVSimDbContext context, JsonElement loadIndex)
{
var cfg = await context.GameConfigurations.FirstOrDefaultAsync(g => g.Id == "default");
if (cfg is null)
{
Console.Error.WriteLine("[GlobalsImporter] GameConfigurations 'default' row missing; " +
"DefaultSettingsSeeder should have created it. Skipping extensions.");
return 0;
}
// Reads the prod capture and overwrites the Rotation and (optionally) Challenge sections
// in GameConfigs. Sections are atomic — we read the existing row (or shipped defaults if
// none), mutate, then serialize back to ValueJson. Each section is one row in GameConfigs.
int touched = 0;
// TODO: fixed in Task 6 — writes through Config tree after RefactorGameConfigurationToJsonb
cfg.Config.Rotation.TsRotationId = GetString(loadIndex, "ts_rotation_id");
cfg.Config.Rotation.IsBattlePassPeriod = GetBool(loadIndex, "is_battle_pass_period");
cfg.Config.Rotation.IsBeginnerMission = GetBool(loadIndex, "is_beginner_mission");
cfg.Config.Rotation.CardSetIdForResourceDlView = GetInt(loadIndex, "card_set_id_for_resource_dl_view");
await UpsertSection<RotationConfig>(context, RotationConfig.ShippedDefaults, rot =>
{
rot.TsRotationId = GetString(loadIndex, "ts_rotation_id");
rot.IsBattlePassPeriod = GetBool(loadIndex, "is_battle_pass_period");
rot.IsBeginnerMission = GetBool(loadIndex, "is_beginner_mission");
rot.CardSetIdForResourceDlView = GetInt(loadIndex, "card_set_id_for_resource_dl_view");
Console.WriteLine($"[GlobalsImporter] GameConfigs/Rotation: ts_rotation_id={rot.TsRotationId}");
});
touched++;
if (loadIndex.TryGetProperty("challenge_config", out var cc))
{
cfg.Config.Challenge.UseTwoPickPremiumCard = GetBool(cc, "use_challenge_two_pick_premium_card");
cfg.Config.Challenge.TwoPickSleeveId = GetLong(cc, "challenge_two_pick_sleeve_id");
await UpsertSection<ChallengeConfig>(context, ChallengeConfig.ShippedDefaults, ch =>
{
ch.UseTwoPickPremiumCard = GetBool(cc, "use_challenge_two_pick_premium_card");
ch.TwoPickSleeveId = GetLong(cc, "challenge_two_pick_sleeve_id");
});
touched++;
}
Console.WriteLine($"[GlobalsImporter] GameConfiguration extensions: ts_rotation_id={cfg.Config.Rotation.TsRotationId}");
return 1;
// my_rotation_info.schedules → MyRotationSchedule section. Two named windows, hard-typed
// on both wire and client (Wizard/MyRotationAllInfo.cs:178-192 reads "gathering" and
// "free_battle" by name and binds them to typed fields). Only upsert when both windows
// parse to real DateTimes — a missing or 0001-01-01 capture would lock the feature off,
// which is exactly the bug the section was added to fix.
if (loadIndex.TryGetProperty("my_rotation_info", out var mri)
&& mri.TryGetProperty("schedules", out var schedules))
{
bool gOk = TryParseScheduleWindow(schedules, "gathering", out var gBegin, out var gEnd);
bool fOk = TryParseScheduleWindow(schedules, "free_battle", out var fBegin, out var fEnd);
if (gOk && fOk)
{
await UpsertSection<MyRotationScheduleConfig>(context, MyRotationScheduleConfig.ShippedDefaults, mr =>
{
mr.Gathering = new ScheduleWindow { Begin = gBegin, End = gEnd };
mr.FreeBattle = new ScheduleWindow { Begin = fBegin, End = fEnd };
Console.WriteLine($"[GlobalsImporter] GameConfigs/MyRotationSchedule: free_battle {fBegin:u} → {fEnd:u}");
});
touched++;
}
else
{
Console.Error.WriteLine("[GlobalsImporter] my_rotation_info.schedules missing or malformed — keeping existing/shipped MyRotationSchedule.");
}
}
return touched;
}
private static bool TryParseScheduleWindow(JsonElement schedules, string key, out DateTime begin, out DateTime end)
{
begin = default;
end = default;
if (!schedules.TryGetProperty(key, out var window) || window.ValueKind != JsonValueKind.Object) return false;
if (!DateTime.TryParse(GetString(window, "begin_time"), out begin)) return false;
if (!DateTime.TryParse(GetString(window, "end_time"), out end)) return false;
return true;
}
private static async Task UpsertSection<T>(SVSimDbContext context, Func<T> shippedDefaults, Action<T> mutate)
where T : class, new()
{
var sectionName = typeof(T).GetCustomAttributes(typeof(ConfigSectionAttribute), inherit: false)
.Cast<ConfigSectionAttribute>().FirstOrDefault()?.Name
?? throw new InvalidOperationException($"{typeof(T).Name} is missing [ConfigSection].");
var row = await context.GameConfigs.FirstOrDefaultAsync(s => s.SectionName == sectionName);
T value;
if (row is null)
{
value = shippedDefaults();
row = new GameConfigSection { SectionName = sectionName };
context.GameConfigs.Add(row);
}
else
{
value = JsonSerializer.Deserialize<T>(row.ValueJson) ?? shippedDefaults();
}
mutate(value);
row.ValueJson = JsonSerializer.Serialize(value);
}
// ---------- My Rotation ----------

View File

@@ -0,0 +1,307 @@
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Models;
namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Idempotently upserts the 9 reference-data tables (classes, leader skins, sleeves, emblems,
/// degrees, battlefields, my-page backgrounds, ranks, class-XP) from CSVs under
/// <c>{AppContext.BaseDirectory}/Data/</c>. Order within ImportAllAsync respects FK
/// dependencies (Classes before LeaderSkins).
/// </summary>
public class ReferenceDataImporter
{
public async Task ImportAllAsync(SVSimDbContext context, string dataDir)
{
if (!Directory.Exists(dataDir))
{
Console.Error.WriteLine($"[ReferenceDataImporter] Data dir missing: {dataDir}");
return;
}
Console.WriteLine($"[ReferenceDataImporter] Reading CSVs from {dataDir}...");
await ImportClasses(context, dataDir);
await ImportLeaderSkins(context, dataDir);
await ImportSleeves(context, dataDir);
await ImportEmblems(context, dataDir);
await ImportDegrees(context, dataDir);
await ImportBattlefields(context, dataDir);
await ImportMyPageBackgrounds(context, dataDir);
await ImportRankInfo(context, dataDir);
await ImportClassExp(context, dataDir);
Console.WriteLine("[ReferenceDataImporter] Done.");
}
private static async Task ImportClasses(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<ClassEntry, ClassEntryMap>(dir, "classes.csv");
var existing = await ctx.Classes.ToDictionaryAsync(c => c.Id);
int created = 0, updated = 0;
foreach (var r in rows)
{
if (existing.TryGetValue(r.Id, out var e))
{
if (e.Name != r.Name) { e.Name = r.Name; updated++; }
}
else { ctx.Classes.Add(r); created++; }
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] Classes: +{created} / ~{updated}");
}
private static async Task ImportLeaderSkins(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<LeaderSkinEntry, LeaderSkinEntryMap>(dir, "leaderskins.csv");
// CSV writes class_chara_id=0 for neutral/unassigned; the FK column is nullable.
foreach (var r in rows) if (r.ClassId == 0) r.ClassId = null;
var existing = await ctx.LeaderSkins.ToDictionaryAsync(s => s.Id);
int created = 0, updated = 0;
foreach (var r in rows)
{
if (existing.TryGetValue(r.Id, out var e))
{
bool changed = false;
if (e.Name != r.Name) { e.Name = r.Name; changed = true; }
if (e.ClassId != r.ClassId) { e.ClassId = r.ClassId; changed = true; }
if (changed) updated++;
}
else { ctx.LeaderSkins.Add(r); created++; }
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] LeaderSkins: +{created} / ~{updated}");
}
private static async Task ImportSleeves(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<SleeveEntry, SleeveEntryMap>(dir, "sleeves.csv");
var existing = (await ctx.Sleeves.ToListAsync()).ToHashSet();
int created = 0;
foreach (var r in rows)
{
if (existing.Any(e => e.Id == r.Id)) continue;
ctx.Sleeves.Add(r); created++;
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] Sleeves: +{created}");
}
private static async Task ImportEmblems(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<EmblemEntry, EmblemEntryMap>(dir, "emblems.csv");
var existing = (await ctx.Emblems.Select(e => e.Id).ToListAsync()).ToHashSet();
int created = 0;
foreach (var r in rows)
{
if (existing.Contains(r.Id)) continue;
ctx.Emblems.Add(r); created++;
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] Emblems: +{created}");
}
private static async Task ImportDegrees(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<DegreeEntry, DegreeEntryMap>(dir, "degrees.csv");
var existing = (await ctx.Degrees.Select(e => e.Id).ToListAsync()).ToHashSet();
int created = 0;
foreach (var r in rows)
{
if (existing.Contains(r.Id)) continue;
ctx.Degrees.Add(r); created++;
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] Degrees: +{created}");
}
private static async Task ImportBattlefields(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<BattlefieldEntry, BattlefieldEntryMap>(dir, "battlefields.csv");
var existing = await ctx.Battlefields.ToDictionaryAsync(b => b.Id);
int created = 0, updated = 0;
foreach (var r in rows)
{
if (existing.TryGetValue(r.Id, out var e))
{
if (e.IsOpen != r.IsOpen) { e.IsOpen = r.IsOpen; updated++; }
}
else { ctx.Battlefields.Add(r); created++; }
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] Battlefields: +{created} / ~{updated}");
}
private static async Task ImportMyPageBackgrounds(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<MyPageBackgroundEntry, MyPageBackgroundEntryMap>(dir, "mypagebackgrounds.csv");
var existing = (await ctx.MyPageBackgrounds.Select(e => e.Id).ToListAsync()).ToHashSet();
int created = 0;
foreach (var r in rows)
{
if (existing.Contains(r.Id)) continue;
ctx.MyPageBackgrounds.Add(r); created++;
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] MyPageBackgrounds: +{created}");
}
private static async Task ImportRankInfo(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<RankInfoEntry, RankInfoEntryMap>(dir, "ranks.csv");
var existing = await ctx.RankInfo.ToDictionaryAsync(r => r.Id);
int created = 0, updated = 0;
foreach (var r in rows)
{
if (existing.TryGetValue(r.Id, out var e))
{
if (ApplyRankUpdates(e, r)) updated++;
}
else { ctx.RankInfo.Add(r); created++; }
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] RankInfo: +{created} / ~{updated}");
}
private static bool ApplyRankUpdates(RankInfoEntry e, RankInfoEntry r)
{
bool changed = false;
if (e.Name != r.Name) { e.Name = r.Name; changed = true; }
if (e.NecessaryPoint != r.NecessaryPoint) { e.NecessaryPoint = r.NecessaryPoint; changed = true; }
if (e.AccumulatePoint != r.AccumulatePoint) { e.AccumulatePoint = r.AccumulatePoint; changed = true; }
if (e.LowerLimitPoint != r.LowerLimitPoint) { e.LowerLimitPoint = r.LowerLimitPoint; changed = true; }
if (e.BaseAddBp != r.BaseAddBp) { e.BaseAddBp = r.BaseAddBp; changed = true; }
if (e.BaseDropBp != r.BaseDropBp) { e.BaseDropBp = r.BaseDropBp; changed = true; }
if (e.StreakBonusPt != r.StreakBonusPt) { e.StreakBonusPt = r.StreakBonusPt; changed = true; }
if (e.WinBonus != r.WinBonus) { e.WinBonus = r.WinBonus; changed = true; }
if (e.LoseBonus != r.LoseBonus) { e.LoseBonus = r.LoseBonus; changed = true; }
if (e.MaxWinBonus != r.MaxWinBonus) { e.MaxWinBonus = r.MaxWinBonus; changed = true; }
if (e.MaxLoseBonus != r.MaxLoseBonus) { e.MaxLoseBonus = r.MaxLoseBonus; changed = true; }
if (e.IsPromotionWar != r.IsPromotionWar) { e.IsPromotionWar = r.IsPromotionWar; changed = true; }
if (e.MatchCount != r.MatchCount) { e.MatchCount = r.MatchCount; changed = true; }
if (e.NecessaryWin != r.NecessaryWin) { e.NecessaryWin = r.NecessaryWin; changed = true; }
if (e.ResetLose != r.ResetLose) { e.ResetLose = r.ResetLose; changed = true; }
if (e.AccumulateMasterPoint != r.AccumulateMasterPoint) { e.AccumulateMasterPoint = r.AccumulateMasterPoint; changed = true; }
return changed;
}
private static async Task ImportClassExp(SVSimDbContext ctx, string dir)
{
var rows = ReadCsv<ClassExpEntry, ClassExpEntryMap>(dir, "classexp.csv");
var existing = await ctx.ClassExpCurve.ToDictionaryAsync(c => c.Id);
int created = 0, updated = 0;
foreach (var r in rows)
{
if (existing.TryGetValue(r.Id, out var e))
{
if (e.NecessaryExp != r.NecessaryExp) { e.NecessaryExp = r.NecessaryExp; updated++; }
}
else { ctx.ClassExpCurve.Add(r); created++; }
}
await ctx.SaveChangesAsync();
Console.WriteLine($"[ReferenceDataImporter] ClassExp: +{created} / ~{updated}");
}
private static List<T> ReadCsv<T, TMap>(string dir, string fileName) where TMap : ClassMap<T>, new()
{
string path = Path.Combine(dir, fileName);
if (!File.Exists(path))
{
Console.Error.WriteLine($"[ReferenceDataImporter] Missing CSV: {path}");
return new List<T>();
}
using var reader = new StreamReader(path);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<TMap>();
return csv.GetRecords<T>().ToList();
}
private sealed class ClassEntryMap : ClassMap<ClassEntry>
{
public ClassEntryMap()
{
Map(m => m.Id).Name("id");
Map(m => m.Name).Name("name");
Map(m => m.DefaultLeaderSkin).Ignore();
}
}
private sealed 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 sealed class EmblemEntryMap : ClassMap<EmblemEntry>
{
public EmblemEntryMap() { Map(m => m.Id).Name("emblem_id"); }
}
private sealed class SleeveEntryMap : ClassMap<SleeveEntry>
{
public SleeveEntryMap() { Map(m => m.Id).Name("sleeve_id"); }
}
private sealed class DegreeEntryMap : ClassMap<DegreeEntry>
{
public DegreeEntryMap() { Map(m => m.Id).Name("degree_id"); }
}
private sealed class BattlefieldEntryMap : ClassMap<BattlefieldEntry>
{
public BattlefieldEntryMap()
{
Map(m => m.Id).Name("value");
Map(m => m.IsOpen).Name("is_open");
}
}
private sealed class MyPageBackgroundEntryMap : ClassMap<MyPageBackgroundEntry>
{
public MyPageBackgroundEntryMap() { Map(m => m.Id).Name("id"); }
}
private sealed class ClassExpEntryMap : ClassMap<ClassExpEntry>
{
public ClassExpEntryMap()
{
Map(m => m.Id).Name("level");
Map(m => m.NecessaryExp).Name("necessary_exp");
}
}
private sealed 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");
}
}
}