refactor(bootstrap): migrate /pack/info to seed file

This commit is contained in:
gamer147
2026-05-26 15:02:49 -04:00
parent 83298a2d47
commit a71bf6c62b
12 changed files with 3287 additions and 192 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
{
"data_headers": { "sid": "fixture", "short_udid": 1, "viewer_id": 1, "servertime": 1779591187, "result_code": 1 },
"data": {
"pack_config_list": [
{
"parent_gacha_id": 10001, "base_pack_id": 10001, "override_draw_effect_pack_id": 10001,
"override_ui_effect_pack_id": 10001, "gacha_type": 1, "sleeve_id": 3000011, "special_sleeve_id": 0,
"commence_date": "2015-04-01 00:00:00", "complete_date": "2030-12-31 23:59:59",
"cardpack_banner_list": [], "gacha_detail": "A pack contains 8 cards, including at least one silver, gold, or legendary card.",
"child_gacha_info": [
{ "gacha_id": 100002, "type_detail": 2, "cost": 100, "count": 8, "override_increase_gacha_point": "1" },
{ "gacha_id": 200001, "type_detail": 3, "cost": 50, "count": 8, "override_increase_gacha_point": "1", "is_daily_single": true },
{ "gacha_id": 400002, "type_detail": 7, "cost": 100, "count": 8, "override_increase_gacha_point": "1" }
],
"open_count": 0, "open_count_limit": 0, "is_hide": 0, "pack_category": 0,
"gacha_point": { "pack_id": "10001", "gacha_point": 0, "increase_gacha_point": "1", "exchangeable_gacha_point": 400, "is_exchangeable_gacha_point": false },
"is_pre_release": false, "exists_purchase_reward": false, "is_new": false, "sales_period_info": [], "poster_type": 0
},
{
"parent_gacha_id": 92001, "base_pack_id": 90001, "override_draw_effect_pack_id": 90001,
"override_ui_effect_pack_id": 90001, "gacha_type": 1, "sleeve_id": 5090001, "special_sleeve_id": 0,
"commence_date": "2017-06-14 10:00:00", "complete_date": "2030-12-31 23:59:59",
"cardpack_banner_list": [], "gacha_detail": "A pack contains 8 cards, including at least one leader card!",
"child_gacha_info": [
{ "gacha_id": 920002, "type_detail": 5, "cost": 1, "count": 8, "item_id": "92001", "item_number": 0 }
],
"open_count": 0, "open_count_limit": 0, "is_hide": 1, "pack_category": 1, "gacha_point": null,
"is_pre_release": false, "exists_purchase_reward": false, "is_new": false, "sales_period_info": [], "poster_type": 0
},
{
"parent_gacha_id": 16015, "base_pack_id": 10015, "override_draw_effect_pack_id": 10015,
"override_ui_effect_pack_id": 10015, "gacha_type": 1, "sleeve_id": 5010015, "special_sleeve_id": 0,
"commence_date": "2017-07-01 03:00:00", "complete_date": "2030-12-31 23:59:59",
"cardpack_banner_list": [
{ "banner_name": "card_pack_711331010_dialog", "dialog_title": "Dia_BuyCard_005_Title" }
],
"gacha_detail": "A pack contains 8 cards, including at least one silver, gold, or legendary card.",
"child_gacha_info": [
{ "gacha_id": 160152, "type_detail": 2, "cost": 100, "count": 8, "override_increase_gacha_point": "1" },
{ "gacha_id": 460152, "type_detail": 7, "cost": 100, "count": 8, "override_increase_gacha_point": "1" }
],
"open_count": 0, "open_count_limit": 0, "is_hide": 0, "pack_category": 0,
"gacha_point": { "pack_id": "10015", "gacha_point": 0, "increase_gacha_point": "1", "exchangeable_gacha_point": 400, "is_exchangeable_gacha_point": false },
"is_pre_release": false, "exists_purchase_reward": false, "is_new": false, "sales_period_info": [], "poster_type": 0
}
]
}
}

View File

@@ -0,0 +1,154 @@
[
{
"parent_gacha_id": 10001,
"base_pack_id": 10001,
"gacha_type": 1,
"pack_category": 0,
"poster_type": 0,
"commence_date": "2015-04-01 00:00:00",
"complete_date": "2030-12-31 23:59:59",
"sleeve_id": 3000011,
"special_sleeve_id": 0,
"override_draw_effect_pack_id": 10001,
"override_ui_effect_pack_id": 10001,
"gacha_detail": "A pack contains 8 cards, including at least one silver, gold, or legendary card.",
"is_hide": false,
"is_new": false,
"is_pre_release": false,
"open_count_limit": 0,
"sales_period_time": null,
"gacha_point": {
"exchangeable_point": 400,
"increase_gacha_point": 1
},
"child_gachas": [
{
"gacha_id": 100002,
"type_detail": 2,
"cost": 100,
"card_count": 8,
"item_id": null,
"is_daily_single": false,
"override_increase_gacha_point": 1,
"purchase_limit_count": 0,
"free_gacha_campaign_id": null,
"campaign_name": null
},
{
"gacha_id": 200001,
"type_detail": 3,
"cost": 50,
"card_count": 8,
"item_id": null,
"is_daily_single": true,
"override_increase_gacha_point": 1,
"purchase_limit_count": 0,
"free_gacha_campaign_id": null,
"campaign_name": null
},
{
"gacha_id": 400002,
"type_detail": 7,
"cost": 100,
"card_count": 8,
"item_id": null,
"is_daily_single": false,
"override_increase_gacha_point": 1,
"purchase_limit_count": 0,
"free_gacha_campaign_id": null,
"campaign_name": null
}
],
"banners": []
},
{
"parent_gacha_id": 92001,
"base_pack_id": 90001,
"gacha_type": 1,
"pack_category": 1,
"poster_type": 0,
"commence_date": "2017-06-14 10:00:00",
"complete_date": "2030-12-31 23:59:59",
"sleeve_id": 5090001,
"special_sleeve_id": 0,
"override_draw_effect_pack_id": 90001,
"override_ui_effect_pack_id": 90001,
"gacha_detail": "A pack contains 8 cards, including at least one leader card!",
"is_hide": true,
"is_new": false,
"is_pre_release": false,
"open_count_limit": 0,
"sales_period_time": null,
"gacha_point": null,
"child_gachas": [
{
"gacha_id": 920002,
"type_detail": 5,
"cost": 1,
"card_count": 8,
"item_id": 92001,
"is_daily_single": false,
"override_increase_gacha_point": 0,
"purchase_limit_count": 0,
"free_gacha_campaign_id": null,
"campaign_name": null
}
],
"banners": []
},
{
"parent_gacha_id": 16015,
"base_pack_id": 10015,
"gacha_type": 1,
"pack_category": 0,
"poster_type": 0,
"commence_date": "2017-07-01 03:00:00",
"complete_date": "2030-12-31 23:59:59",
"sleeve_id": 5010015,
"special_sleeve_id": 0,
"override_draw_effect_pack_id": 10015,
"override_ui_effect_pack_id": 10015,
"gacha_detail": "A pack contains 8 cards, including at least one silver, gold, or legendary card.",
"is_hide": false,
"is_new": false,
"is_pre_release": false,
"open_count_limit": 0,
"sales_period_time": null,
"gacha_point": {
"exchangeable_point": 400,
"increase_gacha_point": 1
},
"child_gachas": [
{
"gacha_id": 160152,
"type_detail": 2,
"cost": 100,
"card_count": 8,
"item_id": null,
"is_daily_single": false,
"override_increase_gacha_point": 1,
"purchase_limit_count": 0,
"free_gacha_campaign_id": null,
"campaign_name": null
},
{
"gacha_id": 460152,
"type_detail": 7,
"cost": 100,
"card_count": 8,
"item_id": null,
"is_daily_single": false,
"override_increase_gacha_point": 1,
"purchase_limit_count": 0,
"free_gacha_campaign_id": null,
"campaign_name": null
}
],
"banners": [
{
"banner_name": "card_pack_711331010_dialog",
"dialog_title": "Dia_BuyCard_005_Title"
}
]
}
]

View File

@@ -1,7 +1,6 @@
using System.Text.Json;
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;
@@ -10,8 +9,8 @@ namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Imports prod-captured globals from <c>{capturesDir}/{endpoint}-*.json</c> snapshots into the
/// DB via idempotent upserts. Source endpoints: <c>load-index</c>, <c>pack-info</c>. Per-endpoint
/// seed-file importers (DefaultDeckImporter, MyPageGlobalsImporter, etc.) cover the rest.
/// DB via idempotent upserts. Source endpoints: <c>load-index</c>. Per-endpoint seed-file
/// importers (DefaultDeckImporter, PackImporter, MyPageGlobalsImporter, etc.) cover the rest.
///
/// Topological order: GameConfiguration extensions → standalone tables → card-referencing tables →
/// rotation CardSet flag update. Card-referencing importers warn on orphans (missing card rows)
@@ -28,7 +27,6 @@ public class GlobalsImporter
Console.WriteLine($"[GlobalsImporter] Loading captures from {capturesDir}...");
JsonElement? loadIndex = LoadCapture(capturesDir, "load-index");
JsonElement? packInfo = LoadCapture(capturesDir, "pack-info");
int total = 0;
@@ -50,11 +48,6 @@ public class GlobalsImporter
total += await UpdateRotationCardSetFlags(context, loadIndex.Value);
}
if (packInfo.HasValue)
{
total += await ImportPacks(context, packInfo.Value);
}
await context.SaveChangesAsync();
Console.WriteLine($"[GlobalsImporter] Done: {total} total rows changed.");
return total;
@@ -506,122 +499,6 @@ public class GlobalsImporter
return updated;
}
// ---------- Pack catalog ----------
/// <summary>
/// Imports /pack/info's pack_config_list into PackConfigEntry rows. The capture's <c>data</c>
/// element wraps an object with a <c>pack_config_list</c> array; iterate that. Owned children
/// (child_gacha_info, cardpack_banner_list) are replaced wholesale on re-runs — diffing
/// owned collections by composite keys is more code than it's worth for catalog updates.
/// </summary>
private async Task<int> ImportPacks(SVSimDbContext context, JsonElement packData)
{
if (!packData.TryGetProperty("pack_config_list", out var list) || list.ValueKind != JsonValueKind.Array)
{
Console.Error.WriteLine("[GlobalsImporter] pack-info capture missing 'pack_config_list'");
return 0;
}
var existing = await context.Packs
.Include(p => p.ChildGachas)
.Include(p => p.Banners)
.ToDictionaryAsync(p => p.Id);
int created = 0, updated = 0;
foreach (var el in list.EnumerateArray())
{
int parentId = GetInt(el, "parent_gacha_id");
if (parentId == 0) continue;
var pack = existing.TryGetValue(parentId, out var ex) ? ex : new PackConfigEntry { Id = parentId };
pack.BasePackId = GetInt(el, "base_pack_id");
pack.GachaType = GetInt(el, "gacha_type");
pack.PackCategory = (PackCategory)GetInt(el, "pack_category");
pack.PosterType = GetInt(el, "poster_type");
pack.CommenceDate = ParseWireDateTime(GetString(el, "commence_date"));
pack.CompleteDate = ParseWireDateTime(GetString(el, "complete_date"));
pack.SleeveId = GetInt(el, "sleeve_id");
pack.SpecialSleeveId = GetInt(el, "special_sleeve_id");
pack.OverrideDrawEffectPackId = GetInt(el, "override_draw_effect_pack_id");
pack.OverrideUiEffectPackId = GetInt(el, "override_ui_effect_pack_id");
pack.GachaDetail = GetString(el, "gacha_detail");
pack.IsHide = GetBool(el, "is_hide");
pack.IsNew = GetBool(el, "is_new");
pack.IsPreRelease = GetBool(el, "is_pre_release");
pack.OpenCountLimit = GetInt(el, "open_count_limit");
// sales_period_info is `{}` when set (object with sales_period_time) and `[]` when unset
if (el.TryGetProperty("sales_period_info", out var spi) && spi.ValueKind == JsonValueKind.Object)
{
var raw = GetString(spi, "sales_period_time");
pack.SalesPeriodTime = string.IsNullOrEmpty(raw) ? null : ParseWireDateTime(raw);
}
else
{
pack.SalesPeriodTime = null;
}
// gacha_point is null when the pack doesn't participate
if (el.TryGetProperty("gacha_point", out var gp) && gp.ValueKind == JsonValueKind.Object)
{
pack.GachaPointConfig = new PackGachaPointConfig
{
ExchangeablePoint = GetInt(gp, "exchangeable_gacha_point"),
IncreaseGachaPoint = GetInt(gp, "increase_gacha_point"),
};
}
else
{
pack.GachaPointConfig = null;
}
// Replace owned collections wholesale.
pack.ChildGachas.Clear();
if (el.TryGetProperty("child_gacha_info", out var cg) && cg.ValueKind == JsonValueKind.Array)
{
foreach (var c in cg.EnumerateArray())
{
pack.ChildGachas.Add(new PackChildGachaEntry
{
GachaId = GetInt(c, "gacha_id"),
TypeDetail = GetInt(c, "type_detail"),
Cost = GetInt(c, "cost"),
CardCount = GetInt(c, "count", 8),
ItemId = c.TryGetProperty("item_id", out var ii) && ii.ValueKind != JsonValueKind.Null
? GetLong(c, "item_id") : (long?)null,
IsDailySingle = GetBool(c, "is_daily_single"),
OverrideIncreaseGachaPoint = GetInt(c, "override_increase_gacha_point"),
PurchaseLimitCount = GetInt(c, "purchase_limit_count"),
FreeGachaCampaignId = c.TryGetProperty("free_gacha_campaign_id", out var fc) && fc.ValueKind != JsonValueKind.Null
? GetInt(c, "free_gacha_campaign_id") : (int?)null,
CampaignName = c.TryGetProperty("campaign_name", out var cn) && cn.ValueKind == JsonValueKind.String
? cn.GetString() : null,
});
}
}
pack.Banners.Clear();
if (el.TryGetProperty("cardpack_banner_list", out var bl) && bl.ValueKind == JsonValueKind.Array)
{
foreach (var b in bl.EnumerateArray())
{
pack.Banners.Add(new PackBannerEntry
{
BannerName = GetString(b, "banner_name"),
DialogTitle = GetString(b, "dialog_title"),
});
}
}
if (ex is null) { context.Packs.Add(pack); created++; }
else updated++;
}
Console.WriteLine($"[GlobalsImporter] Packs: +{created}/~{updated}");
return created + updated;
}
// ---------- Helpers ----------
private static void WarnOrphans(string label, int count)

View File

@@ -0,0 +1,107 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Bootstrap.Models.Seed;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using static SVSim.Bootstrap.Importers.ImporterBase;
namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Idempotent upsert of /pack/info catalog from <c>seeds/packs.json</c>. Owned collections
/// (ChildGachas, Banners) are replaced wholesale per pack (clear-then-rehydrate) -- diffing owned
/// collections by composite keys is more code than it's worth for catalog updates, and this
/// matches the wholesale-replace semantics of the previous in-line ImportPacks implementation.
/// Rows missing from the seed are LEFT INTACT.
/// </summary>
public class PackImporter
{
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
{
var seed = SeedLoader.LoadList<PackSeed>(Path.Combine(seedDir, "packs.json"));
if (seed.Count == 0)
{
Console.WriteLine("[PackImporter] No seed rows; skipping.");
return 0;
}
var existing = await context.Packs
.Include(p => p.ChildGachas)
.Include(p => p.Banners)
.ToDictionaryAsync(p => p.Id);
int created = 0, updated = 0;
foreach (var s in seed)
{
if (s.ParentGachaId == 0) continue;
var pack = existing.TryGetValue(s.ParentGachaId, out var ex)
? ex : new PackConfigEntry { Id = s.ParentGachaId };
pack.BasePackId = s.BasePackId;
pack.GachaType = s.GachaType;
pack.PackCategory = (PackCategory)s.PackCategory;
pack.PosterType = s.PosterType;
pack.CommenceDate = ParseWireDateTime(s.CommenceDate);
pack.CompleteDate = ParseWireDateTime(s.CompleteDate);
pack.SleeveId = s.SleeveId;
pack.SpecialSleeveId = s.SpecialSleeveId;
pack.OverrideDrawEffectPackId = s.OverrideDrawEffectPackId;
pack.OverrideUiEffectPackId = s.OverrideUiEffectPackId;
pack.GachaDetail = s.GachaDetail;
pack.IsHide = s.IsHide;
pack.IsNew = s.IsNew;
pack.IsPreRelease = s.IsPreRelease;
pack.OpenCountLimit = s.OpenCountLimit;
pack.SalesPeriodTime = string.IsNullOrEmpty(s.SalesPeriodTime)
? null
: ParseWireDateTime(s.SalesPeriodTime);
pack.GachaPointConfig = s.GachaPoint is null ? null : new PackGachaPointConfig
{
ExchangeablePoint = s.GachaPoint.ExchangeablePoint,
IncreaseGachaPoint = s.GachaPoint.IncreaseGachaPoint,
};
// Owned collections -- clear and rehydrate (matches the previous wholesale-replace semantics).
pack.ChildGachas.Clear();
foreach (var c in s.ChildGachas)
{
pack.ChildGachas.Add(new PackChildGachaEntry
{
GachaId = c.GachaId,
TypeDetail = c.TypeDetail,
Cost = c.Cost,
CardCount = c.CardCount,
ItemId = c.ItemId,
IsDailySingle = c.IsDailySingle,
OverrideIncreaseGachaPoint = c.OverrideIncreaseGachaPoint,
PurchaseLimitCount = c.PurchaseLimitCount,
FreeGachaCampaignId = c.FreeGachaCampaignId,
CampaignName = c.CampaignName,
});
}
pack.Banners.Clear();
foreach (var b in s.Banners)
{
pack.Banners.Add(new PackBannerEntry
{
BannerName = b.BannerName,
DialogTitle = b.DialogTitle,
});
}
if (ex is null)
{
context.Packs.Add(pack);
existing[s.ParentGachaId] = pack;
created++;
}
else updated++;
}
await context.SaveChangesAsync();
Console.WriteLine($"[PackImporter] +{created}/~{updated}");
return created + updated;
}
}

View File

@@ -0,0 +1,53 @@
using System.Text.Json.Serialization;
namespace SVSim.Bootstrap.Models.Seed;
public sealed class PackSeed
{
[JsonPropertyName("parent_gacha_id")] public int ParentGachaId { get; set; }
[JsonPropertyName("base_pack_id")] public int BasePackId { get; set; }
[JsonPropertyName("gacha_type")] public int GachaType { get; set; }
[JsonPropertyName("pack_category")] public int PackCategory { get; set; }
[JsonPropertyName("poster_type")] public int PosterType { get; set; }
[JsonPropertyName("commence_date")] public string CommenceDate { get; set; } = "";
[JsonPropertyName("complete_date")] public string CompleteDate { get; set; } = "";
[JsonPropertyName("sleeve_id")] public int SleeveId { get; set; }
[JsonPropertyName("special_sleeve_id")] public int SpecialSleeveId { get; set; }
[JsonPropertyName("override_draw_effect_pack_id")] public int OverrideDrawEffectPackId { get; set; }
[JsonPropertyName("override_ui_effect_pack_id")] public int OverrideUiEffectPackId { get; set; }
[JsonPropertyName("gacha_detail")] public string GachaDetail { get; set; } = "";
[JsonPropertyName("is_hide")] public bool IsHide { get; set; }
[JsonPropertyName("is_new")] public bool IsNew { get; set; }
[JsonPropertyName("is_pre_release")] public bool IsPreRelease { get; set; }
[JsonPropertyName("open_count_limit")] public int OpenCountLimit { get; set; }
[JsonPropertyName("sales_period_time")] public string? SalesPeriodTime { get; set; }
[JsonPropertyName("gacha_point")] public PackGachaPointSeed? GachaPoint { get; set; }
[JsonPropertyName("child_gachas")] public List<PackChildGachaSeed> ChildGachas { get; set; } = new();
[JsonPropertyName("banners")] public List<PackBannerSeed> Banners { get; set; } = new();
}
public sealed class PackGachaPointSeed
{
[JsonPropertyName("exchangeable_point")] public int ExchangeablePoint { get; set; }
[JsonPropertyName("increase_gacha_point")] public int IncreaseGachaPoint { get; set; }
}
public sealed class PackChildGachaSeed
{
[JsonPropertyName("gacha_id")] public int GachaId { get; set; }
[JsonPropertyName("type_detail")] public int TypeDetail { get; set; }
[JsonPropertyName("cost")] public int Cost { get; set; }
[JsonPropertyName("card_count")] public int CardCount { get; set; } = 8;
[JsonPropertyName("item_id")] public long? ItemId { get; set; }
[JsonPropertyName("is_daily_single")] public bool IsDailySingle { get; set; }
[JsonPropertyName("override_increase_gacha_point")] public int OverrideIncreaseGachaPoint { get; set; }
[JsonPropertyName("purchase_limit_count")] public int PurchaseLimitCount { get; set; }
[JsonPropertyName("free_gacha_campaign_id")] public int? FreeGachaCampaignId { get; set; }
[JsonPropertyName("campaign_name")] public string? CampaignName { get; set; }
}
public sealed class PackBannerSeed
{
[JsonPropertyName("banner_name")] public string BannerName { get; set; } = "";
[JsonPropertyName("dialog_title")] public string DialogTitle { get; set; } = "";
}

View File

@@ -91,6 +91,7 @@ public static class Program
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