Files
SVSimServer/SVSim.Bootstrap/Program.cs
2026-05-24 21:13:15 -04:00

194 lines
8.1 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)
{
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] Captures: {opts.CapturesDir}");
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)
{
await new GlobalsImporter().ImportAllAsync(context, opts.CapturesDir);
}
else
{
Console.WriteLine("[Bootstrap] --skip-globals set; skipping globals import.");
}
Console.WriteLine("[Bootstrap] Complete.");
return 0;
}
private static BootstrapOptions? ParseArgs(string[] args)
{
string? dataDir = null;
string? cards = null;
string? captures = null;
string? referenceDataDir = null;
string? connection = null;
bool skipReference = false;
bool skipCards = false;
bool skipGlobals = false;
string? positionalCards = null;
for (int i = 0; i < args.Length; i++)
{
string a = args[i];
switch (a)
{
case "--data-dir": dataDir = NextArg(args, ref i); break;
case "--cards": cards = NextArg(args, ref i); break;
case "--captures": captures = 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;
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;
}
}
// Resolution order:
// --cards beats --data-dir/cards.json beats legacy positional;
// --captures beats --data-dir/prod-captures beats Bootstrap/Data/prod-captures (shipped default);
// --reference-data-dir beats shipped Bootstrap/Data (the CSVs always ship next to the binary).
string baseDir = AppContext.BaseDirectory;
string shippedDataDir = Path.Combine(baseDir, "Data");
string shippedCaptures = Path.Combine(shippedDataDir, "prod-captures");
string cardsFile = cards
?? (dataDir is not null ? Path.Combine(dataDir, "cards.json") : null)
?? positionalCards
?? "data_dumps/cards.json";
// Resolve captures dir, falling back to the shipped copy if the data-dir path is unset
// OR points at a missing folder. (Common case: user has cards.json in data_dumps/ but
// hasn't copied prod-captures/ there — the shipped snapshot is the source of truth.)
string? capturesCandidate = captures
?? (dataDir is not null ? Path.Combine(dataDir, "prod-captures") : null);
string capturesDir = capturesCandidate is not null && Directory.Exists(capturesCandidate)
? capturesCandidate
: shippedCaptures;
string refDir = referenceDataDir ?? shippedDataDir;
string connStr = connection
?? Environment.GetEnvironmentVariable("NPGSQL_CONNECTION")
?? DefaultConnectionString;
return new BootstrapOptions(
cardsFile, capturesDir, refDir, connStr, skipReference, skipCards, skipGlobals);
}
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" +
" --data-dir <path> Directory containing cards.json and prod-captures/\n" +
" (default: ./data_dumps relative to working dir)\n" +
" --cards <file> Override path to cards.json\n" +
" --captures <dir> Override path to prod-captures directory\n" +
" (default: shipped Data/prod-captures next to the binary)\n" +
" --reference-data-dir <dir> Override reference CSV directory\n" +
" (default: shipped Data/ next to the binary)\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 prod-captured globals import\n" +
"\n" +
"Back-compat: `svsim-bootstrap <cards.json> [connection]` still works (positional).");
}
private sealed record BootstrapOptions(
string CardsFile,
string CapturesDir,
string ReferenceDataDir,
string ConnectionString,
bool SkipReference,
bool SkipCards,
bool SkipGlobals);
}