using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; namespace SVSim.CardImport; public static class Program { private const string DefaultConnectionString = "Host=localhost;Database=svsim;Username=postgres;password=postgres"; public static async Task Main(string[] args) { if (args.Length < 1 || args[0] is "--help" or "-h") { Console.Error.WriteLine( "Usage: svsim-card-import [connection-string]\n" + "\n" + " cards.json Path to the loader's card dump (LitJson array of CardCSVData)\n" + " connection-string Postgres connection (falls back to NPGSQL_CONNECTION env var,\n" + $" then \"{DefaultConnectionString}\")"); return 1; } string path = args[0]; string connection = args.Length > 1 ? args[1] : Environment.GetEnvironmentVariable("NPGSQL_CONNECTION") ?? DefaultConnectionString; if (!File.Exists(path)) { Console.Error.WriteLine($"File not found: {path}"); return 2; } Console.WriteLine($"Reading {path} ({new FileInfo(path).Length / 1024} KiB)..."); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, NumberHandling = JsonNumberHandling.AllowReadingFromString, }; List? input; await using (var fs = File.OpenRead(path)) { input = await JsonSerializer.DeserializeAsync>(fs, jsonOptions); } if (input is null || input.Count == 0) { Console.Error.WriteLine("No card records parsed from input."); return 3; } Console.WriteLine($"Parsed {input.Count} card records."); var dbOptions = new DbContextOptionsBuilder() .UseNpgsql(connection) .Options; await using var context = new SVSimDbContext(NullLogger.Instance, dbOptions); // Apply any pending migrations first — bootstraps a fresh DB so CardImport can be the // very first thing run after `dotnet ef migrations add` (no need to run the server too). // Migration files have InsertData rows for the seeded master data already; runtime seeder // skip is fine. await context.Database.MigrateAsync(); var classesById = await context.Classes.ToDictionaryAsync(c => c.Id); var existingSets = (await context.CardSets.ToListAsync()).ToDictionary(s => s.Id); var existingCards = (await context.Cards.ToListAsync()).ToDictionary(c => c.Id); Console.WriteLine( $"DB state before: {existingCards.Count} cards, {existingSets.Count} card sets, " + $"{classesById.Count} classes seeded."); int created = 0, updated = 0, skipped = 0, setsCreated = 0; foreach (var c in input) { if (!long.TryParse(c.CardId, out long id) || id == 0) { skipped++; continue; } int setId = ParseInt(c.CardSetId, 0); int clan = ParseInt(c.Clan, 0); int rarity = ParseInt(c.Rarity, 0); if (!existingSets.TryGetValue(setId, out var set)) { set = new ShadowverseCardSetEntry { Id = setId, Name = $"Card Set {setId}", IsInRotation = true, IsBasic = false }; context.CardSets.Add(set); existingSets[setId] = set; setsCreated++; } ClassEntry? classEntry = clan > 0 && classesById.TryGetValue(clan, out var ce) ? ce : null; var collection = new CardCollectionInfo { CraftCost = ParseInt(c.UseRedEther, 0), DustReward = ParseInt(c.GetRedEther, 0) }; if (existingCards.TryGetValue(id, out var card)) { card.Rarity = (Rarity)rarity; card.PrimaryResourceCost = ParseNullableInt(c.Cost); card.Attack = ParseNullableInt(c.Atk); card.Defense = ParseNullableInt(c.Life); card.Class = classEntry; card.CollectionInfo = collection; updated++; } else { card = new ShadowverseCardEntry { Id = id, Name = $"Card {id}", Rarity = (Rarity)rarity, PrimaryResourceCost = ParseNullableInt(c.Cost), Attack = ParseNullableInt(c.Atk), Defense = ParseNullableInt(c.Life), Class = classEntry, CollectionInfo = collection }; set.Cards.Add(card); existingCards[id] = card; created++; } } Console.WriteLine( $"Saving: +{created} cards, ~{updated} updated, +{setsCreated} card sets, " + $"skipped {skipped} (bad/missing card_id)..."); await context.SaveChangesAsync(); Console.WriteLine("Done."); return 0; } private static int ParseInt(string? raw, int fallback) => int.TryParse(raw, out int v) ? v : fallback; private static int? ParseNullableInt(string? raw) => int.TryParse(raw, out int v) ? v : null; } /// /// Lightweight projection over the CardCSVData fields we care about. The dump has many more /// fields (PascalCase metadata + effect/voice/visual paths) — we ignore them; only the /// snake_case CSV columns map here via the SnakeCaseLower naming policy. /// public class CardInput { public string? CardId { get; set; } public string? CardSetId { get; set; } public string? Clan { get; set; } public string? Cost { get; set; } public string? Atk { get; set; } public string? Life { get; set; } public string? Rarity { get; set; } public string? GetRedEther { get; set; } public string? UseRedEther { get; set; } }