176 lines
6.4 KiB
C#
176 lines
6.4 KiB
C#
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<int> Main(string[] args)
|
|
{
|
|
if (args.Length < 1 || args[0] is "--help" or "-h")
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Usage: svsim-card-import <cards.json> [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<CardInput>? input;
|
|
await using (var fs = File.OpenRead(path))
|
|
{
|
|
input = await JsonSerializer.DeserializeAsync<List<CardInput>>(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<SVSimDbContext>()
|
|
.UseNpgsql(connection)
|
|
.Options;
|
|
|
|
await using var context = new SVSimDbContext(NullLogger<SVSimDbContext>.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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; }
|
|
}
|