Files
SVSimServer/SVSim.Bootstrap/Program.cs
gamer147 c02991a5c2 refactor(bootstrap): finalize seed migration; remove GlobalsImporter and prod-captures plumbing
Final cleanup of the bootstrap-seed refactor (Task 10 of 10):

- Delete the GlobalsImporter no-op stub and its two remaining call sites
  (Program.cs and SVSimTestFactory.cs). All work has moved to per-domain
  importers since Task 9.
- Drop the --captures CLI flag and CapturesDir / shippedCaptures plumbing
  from Program.cs (BootstrapOptions, ParseArgs, PrintUsage). Bootstrap input
  is now cards.json + reference CSVs + per-table seed JSON; no more
  prod-captures directory.
- Shrink ImporterBase from 141 to 23 lines: LoadCapture, Serialize,
  Upsert<T,TKey>, GetInt/GetString/GetBool/GetLong/GetULong all had zero
  callers after the seed migration. Only ParseWireDateTime survives (still
  used by PaymentItemImporter and MyPageGlobalsImporter for prod-shaped
  timestamp strings).
- Remove the prod-captures Content Include glob from SVSim.Bootstrap.csproj
  and both prod-captures globs (production + test-fixture overlay) from
  SVSim.UnitTests.csproj. Test fixtures now overlay production seeds at
  Data/seeds/ via the Task 7 layout exclusively.

Build clean; 391/391 unit tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:06:46 -04:00

240 lines
11 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using SVSim.Bootstrap.Importers;
using SVSim.Database;
namespace SVSim.Bootstrap;
public static class Program
{
private const string DefaultConnectionString =
"Host=localhost;Database=svsim;Username=postgres;password=postgres";
public static async Task<int> Main(string[] args)
{
if (args.Length > 0 && (args[0] is "--help" or "-h"))
{
PrintUsage();
return 1;
}
var opts = ParseArgs(args);
if (opts is null)
{
PrintUsage();
return 1;
}
if (opts.SkipReference && opts.SkipCards && opts.SkipGlobals && opts.SkipStory)
{
Console.Error.WriteLine("All --skip-* flags set; nothing to do.");
return 1;
}
Console.WriteLine($"[Bootstrap] Connection: {RedactPassword(opts.ConnectionString)}");
Console.WriteLine($"[Bootstrap] Reference CSVs: {opts.ReferenceDataDir}");
Console.WriteLine($"[Bootstrap] Cards file: {opts.CardsFile}");
Console.WriteLine($"[Bootstrap] Seeds: {opts.SeedDir}");
var dbOptions = new DbContextOptionsBuilder<SVSimDbContext>()
.UseNpgsql(opts.ConnectionString)
.Options;
await using var context = new SVSimDbContext(NullLogger<SVSimDbContext>.Instance, dbOptions);
// Bootstrap applies pending migrations first — migrations are now DDL-only, all data
// (reference tables, cards, card cosmetic rewards, prod-captured globals, game config)
// is loaded by importers below. This means a freshly migrated DB is structure-only;
// every importer is idempotent so re-running is safe.
Console.WriteLine("[Bootstrap] Applying pending migrations...");
await context.Database.MigrateAsync();
// GameConfigSection rows for every [ConfigSection] type — runtime seed (HasData doesn't
// play well with OwnsOne+ToJson). Always run; tiers only insert missing sections.
await context.EnsureSeedDataAsync();
if (!opts.SkipReference)
{
await new ReferenceDataImporter().ImportAllAsync(context, opts.ReferenceDataDir);
}
else
{
Console.WriteLine("[Bootstrap] --skip-reference set; skipping reference data import.");
}
if (!opts.SkipCards)
{
await new CardImporter().ImportAsync(context, opts.CardsFile);
// Card cosmetic rewards FK to Cards; piggy-back on --skip-cards.
await new CardCosmeticRewardImporter().ImportAsync(context, opts.ReferenceDataDir);
}
else
{
Console.WriteLine("[Bootstrap] --skip-cards set; skipping card + cosmetic-reward import.");
}
if (!opts.SkipGlobals)
{
// Per-domain seed pipeline. The legacy GlobalsImporter that parsed prod-captured
// /load/index, /mypage/index, /deck/info wire payloads directly is gone — capture
// → seed transformation lives in data_dumps/extract/*; importers below just
// deserialise the per-table JSON files in SVSim.Bootstrap/Data/seeds/.
//
// RotationConfigImporter writes the Rotation GameConfig section that RotationFlagUpdater
// reads; CardImporter ran earlier in the !SkipCards block so CardSets are populated.
await new RotationConfigImporter().ImportAsync(context, opts.SeedDir);
await new MyRotationImporter().ImportAsync(context, opts.SeedDir);
await new AvatarAbilityImporter().ImportAsync(context, opts.SeedDir);
await new ArenaSeasonImporter().ImportAsync(context, opts.SeedDir);
await new BattlePassImporter().ImportAsync(context, opts.SeedDir);
await new DailyLoginBonusImporter().ImportAsync(context, opts.SeedDir);
await new PreReleaseInfoImporter().ImportAsync(context, opts.SeedDir);
await new CardListsImporter().ImportAsync(context, opts.SeedDir);
await new RotationFlagUpdater().UpdateAsync(context);
await new PracticeOpponentImporter().ImportAsync(context, opts.SeedDir);
await new PaymentItemImporter().ImportAsync(context, opts.SeedDir);
var puzzleImporter = new PuzzleImporter();
await puzzleImporter.ImportGroupsAsync(context, opts.SeedDir);
await puzzleImporter.ImportPuzzlesAsync(context, opts.SeedDir);
await puzzleImporter.ImportMissionsAsync(context, opts.SeedDir);
var mypage = new MyPageGlobalsImporter();
await mypage.ImportBannersAsync(context, opts.SeedDir);
await mypage.ImportColosseumAsync(context, opts.SeedDir);
await mypage.ImportSealedAsync(context, opts.SeedDir);
await mypage.ImportMasterPointRankingPeriodAsync(context, opts.SeedDir);
await mypage.ImportSpecialDeckFormatsAsync(context, opts.SeedDir);
await new DefaultDeckImporter().ImportAsync(context, opts.SeedDir);
await new PackImporter().ImportAsync(context, opts.SeedDir);
// BuildDeck pipeline: series CSV → catalog JSON → package CSV. Catalog must run after
// series CSV (FK on products → series) and before package CSV (so the catalog-side
// enriched rows take precedence over stub creation).
var buildDeck = new BuildDeckImporter();
await buildDeck.ImportSeriesAsync(context, opts.ReferenceDataDir);
await buildDeck.ImportCatalogAsync(context, opts.SeedDir);
await buildDeck.ImportPackageAsync(context, opts.ReferenceDataDir);
}
else
{
Console.WriteLine("[Bootstrap] --skip-globals set; skipping globals import.");
}
if (!opts.SkipStory)
{
await new StoryImporter().ImportAsync(context, opts.StoryDataDir);
}
else
{
Console.WriteLine("[Bootstrap] --skip-story set; skipping story import.");
}
Console.WriteLine("[Bootstrap] Complete.");
return 0;
}
private static BootstrapOptions? ParseArgs(string[] args)
{
string? cards = null;
string? referenceDataDir = null;
string? connection = null;
bool skipReference = false;
bool skipCards = false;
bool skipGlobals = false;
bool skipStory = false;
string? storyDataDir = null;
string? positionalCards = null;
for (int i = 0; i < args.Length; i++)
{
string a = args[i];
switch (a)
{
case "--cards": cards = NextArg(args, ref i); break;
case "--reference-data-dir": referenceDataDir = NextArg(args, ref i); break;
case "--connection-string": connection = NextArg(args, ref i); break;
case "--skip-reference": skipReference = true; break;
case "--skip-cards": skipCards = true; break;
case "--skip-globals": skipGlobals = true; break;
case "--skip-story": skipStory = true; break;
case "--story-data-dir": storyDataDir = NextArg(args, ref i); break;
default:
// Back-compat: legacy positional form `svsim-card-import <cards.json> [connection]`.
if (positionalCards is null && !a.StartsWith('-')) positionalCards = a;
else if (connection is null && !a.StartsWith('-')) connection = a;
else { Console.Error.WriteLine($"Unknown argument: {a}"); return null; }
break;
}
}
// All bootstrap inputs ship in-project under SVSim.Bootstrap/Data/, copied next to the
// binary on build. The --cards/--reference-data-dir flags are ad-hoc overrides
// (e.g. point at a fresh loader dump before promoting it into the project).
string baseDir = AppContext.BaseDirectory;
string shippedDataDir = Path.Combine(baseDir, "Data");
string shippedCardsFile = Path.Combine(shippedDataDir, "cards.json");
string cardsFile = cards ?? positionalCards ?? shippedCardsFile;
string refDir = referenceDataDir ?? shippedDataDir;
string shippedStoryDir = Path.Combine(shippedDataDir, "story");
string storyDir = storyDataDir ?? shippedStoryDir;
string shippedSeedDir = Path.Combine(shippedDataDir, "seeds");
string connStr = connection
?? Environment.GetEnvironmentVariable("NPGSQL_CONNECTION")
?? DefaultConnectionString;
return new BootstrapOptions(
cardsFile, refDir, connStr, skipReference, skipCards, skipGlobals,
skipStory, storyDir, shippedSeedDir);
}
private static string NextArg(string[] args, ref int i)
{
if (i + 1 >= args.Length) throw new ArgumentException($"Missing value for {args[i]}");
return args[++i];
}
private static string RedactPassword(string conn) =>
System.Text.RegularExpressions.Regex.Replace(conn, "(?i)(password=)[^;]+", "$1***");
private static void PrintUsage()
{
Console.Error.WriteLine(
"Usage: svsim-bootstrap [options]\n" +
"\n" +
" All inputs default to the in-project SVSim.Bootstrap/Data/ folder, copied next to\n" +
" the binary at build time. Override flags below take ad-hoc paths (e.g. a fresh\n" +
" loader dump) — promote into Data/ when you're ready to make it permanent.\n" +
"\n" +
" --cards <file> Override path to cards.json (default: shipped Data/cards.json)\n" +
" --reference-data-dir <dir> Override reference CSV directory (default: shipped Data/)\n" +
" --connection-string <conn> Postgres connection (or NPGSQL_CONNECTION env var,\n" +
$" then \"{DefaultConnectionString}\")\n" +
" --skip-reference Skip reference-data import (classes, sleeves, ranks, ...)\n" +
" --skip-cards Skip card + card-cosmetic-reward import\n" +
" --skip-globals Skip seed-driven globals import (per-table JSON under Data/seeds)\n" +
" --story-data-dir <dir> Override story data directory (default: shipped Data/story)\n" +
" --skip-story Skip story import (worlds/sections/chapters/sbs)\n" +
"\n" +
"Capture-derived seeds are produced by extractors under data_dumps/extract/* and\n" +
"checked into SVSim.Bootstrap/Data/seeds/. The bootstrap project never parses wire\n" +
"captures directly — refresh seeds by re-running the relevant extractor.\n" +
"\n" +
"Back-compat: `svsim-bootstrap <cards.json> [connection]` still works (positional).");
}
private sealed record BootstrapOptions(
string CardsFile,
string ReferenceDataDir,
string ConnectionString,
bool SkipReference,
bool SkipCards,
bool SkipGlobals,
bool SkipStory,
string StoryDataDir,
string SeedDir);
}