refactor(bootstrap): migrate mypage-index globals to seed files

This commit is contained in:
gamer147
2026-05-26 14:31:25 -04:00
parent 0da8ebe1c1
commit a5e4f35c32
16 changed files with 561 additions and 368 deletions

View File

@@ -1,229 +0,0 @@
{
"data_headers": {
"short_udid": 411054851,
"viewer_id": 906243102,
"sid": "",
"servertime": 1779553959,
"result_code": 1
},
"data": {
"user_info": {
"device_type": "2",
"name": "combusty7",
"country_code": "KOR",
"max_friend": "20",
"last_play_time": "2026-05-23 16:32:39",
"is_received_two_pick_mission": "1",
"birth": "19600101",
"selected_emblem_id": "701441011",
"selected_degree_id": "300003",
"mission_change_time": "2017-09-17 14:47:13",
"mission_receive_type": "0",
"is_official": "0",
"is_official_mark_displayed": "0"
},
"sealed_info": {
"enable": 1,
"crystal_cost": 600,
"rupy_cost": 600,
"ticket_cost": 4,
"is_join": false,
"pack_info": [
10032,
10032,
10031,
10030,
10029
],
"deck_using_num_min": 30,
"schedule_id": 21,
"is_deck_code_maintenance": false,
"sales_period_info": {
"sales_period_series": 33
}
},
"colosseum_info": {
"colosseum_id": "165",
"is_display_tips": "0",
"tips_id": "0",
"card_pool_name": "Take Two (DragonbladeRivenbrandt)",
"is_colosseum_period": true,
"is_round_period": true,
"deck_format": "3",
"is_normal_two_pick": "1",
"is_special_mode": "10",
"is_all_card_enabled": 0,
"start_time": "2026-05-21 06:00:00",
"colosseum_name": "Rivenbrandt Take Two Cup",
"now_round": "1",
"end_time": "2026-05-25 19:59:59",
"sales_period_info": {
"sales_period_time": "2026-05-25 19:59:59"
}
},
"is_available_colosseum_free_entry": true,
"arena_info": [
{
"mode": 1,
"enable": 1,
"cost": 150,
"rupy_cost": 150,
"ticket_cost": 1,
"is_join": false,
"format_info": {
"two_pick_type": "1",
"card_pool_name": "Take Two (DragonbladeRivenbrandt)",
"announce_id": 0,
"last_card_pack_set_id": "10029",
"start_time": "2026-05-01 02:00:00",
"end_time": "2026-06-01 01:59:59"
},
"sales_period_info": {
"sales_period_time": "2026-06-01 01:59:59"
}
}
],
"is_arena_challenge_period": true,
"is_hidden_boss_appeared": false,
"competition_info": {
"is_competition_period": false
},
"treasure_info": null,
"unread_present_count": 0,
"unreceived_mission_reward_count": 0,
"lottery_period_info": null,
"master_point_ranking_period": {
"id": "119",
"period_num": "118",
"necessary_score": "0",
"begin_time": "2026-05-01 02:00:00",
"end_time": "2026-06-01 01:59:59"
},
"last_announce_id": "3353",
"last_announce_update_time": "2026-05-15 10:22:11",
"unfinished_battle_exists": false,
"is_joined_room": false,
"receive_friend_apply_count": 0,
"feature_maintenance_list": [],
"can_give_daily_login_bonus": false,
"friend_battle_invite_count": 0,
"user_config": {
"receive_invitation": "1",
"receive_invitation_in_battle": "1",
"receive_invitation_in_offline": "1",
"receive_friend_apply": "1",
"is_allow_send_adjust": "1",
"is_foil_preferred": "0",
"is_prize_preferred": "0"
},
"banner": [
{
"image_name": "banner_000788",
"click": "account_transition_with_two",
"status": "10",
"change_time": "10",
"remaining_time": "0",
"image_paths": []
},
{
"image_name": "banner_000906",
"click": "colosseum",
"status": "",
"change_time": "10",
"remaining_time": "0",
"image_paths": []
},
{
"image_name": "banner_000220",
"click": "deck_intro_rotation",
"status": "17",
"change_time": "10",
"remaining_time": "0",
"image_paths": []
},
{
"image_name": "banner_000840",
"click": "mission",
"status": "2",
"change_time": "10",
"remaining_time": "0",
"image_paths": []
}
],
"sub_banner": null,
"sub_banner_list": [],
"user_mypage_info": {
"user_mypage_setting": {
"mypage_id": "0",
"select_type": "0",
"mypage_id_list": []
}
},
"user_offline_event": [],
"convention": {
"is_join_tournament": false,
"recent_start_date": null,
"is_admin_watch_user": false
},
"special_crystal_info": [],
"room_type_in_session": {
"special_deck_format_list": [
{
"deck_format": "5",
"end_time": "2030-06-26 19:59:59"
}
]
},
"guild_notification": {
"guild_id": null,
"guild_room_message_id": null,
"is_join_request": false,
"is_invited": false
},
"shop_notification": {
"card_pack": {
"is_open_free_gacha_campaign": false,
"can_free_gacha": false
},
"build_deck": [],
"sleeve": [],
"leader_skin": []
},
"pre_release_status": 0,
"gathering_info": {
"has_invite": 0,
"is_entry": 0
},
"quest": {
"is_open": false,
"is_display_badge": false,
"is_daily_first_access": false,
"end_time": "",
"name": ""
},
"basic_puzzle": {
"is_display_badge": true
},
"all_card_enabled_period": null,
"user_item_list": [
{
"item_id": "1",
"number": "19"
},
{
"item_id": "10011",
"number": "1"
},
{
"item_id": "80001",
"number": "1"
}
],
"is_battle_pass_period": true,
"story_notification": {
"is_display_ribbon": false,
"is_display_badge": false
},
"home_dialog_list": []
}
}

View File

@@ -0,0 +1,38 @@
[
{
"id": 1,
"image_name": "banner_000788",
"click": "account_transition_with_two",
"status": "10",
"change_time": 10,
"remaining_time": 0,
"image_paths": []
},
{
"id": 2,
"image_name": "banner_000906",
"click": "colosseum",
"status": "",
"change_time": 10,
"remaining_time": 0,
"image_paths": []
},
{
"id": 3,
"image_name": "banner_000220",
"click": "deck_intro_rotation",
"status": "17",
"change_time": 10,
"remaining_time": 0,
"image_paths": []
},
{
"id": 4,
"image_name": "banner_000840",
"click": "mission",
"status": "2",
"change_time": 10,
"remaining_time": 0,
"image_paths": []
}
]

View File

@@ -0,0 +1,20 @@
{
"id": 1,
"colosseum_id": "165",
"colosseum_name": "Rivenbrandt Take Two Cup",
"card_pool_name": "Take Two (DragonbladeRivenbrandt)",
"deck_format": "3",
"start_time": "2026-05-21 06:00:00",
"end_time": "2026-05-25 19:59:59",
"now_round": "1",
"is_display_tips": "0",
"tips_id": "0",
"is_colosseum_period": true,
"is_round_period": true,
"is_normal_two_pick": "1",
"is_special_mode": "10",
"is_all_card_enabled": 0,
"sales_period_info": {
"sales_period_time": "2026-05-25 19:59:59"
}
}

View File

@@ -0,0 +1,9 @@
[
{
"id": 119,
"period_num": 118,
"necessary_score": 0,
"begin_time": "2026-05-01 02:00:00",
"end_time": "2026-06-01 01:59:59"
}
]

View File

@@ -0,0 +1,21 @@
{
"id": 1,
"enable": 1,
"crystal_cost": 600,
"rupy_cost": 600,
"ticket_cost": 4,
"deck_using_num_min": 30,
"schedule_id": 21,
"is_join": false,
"is_deck_code_maintenance": false,
"pack_info": [
10032,
10032,
10031,
10030,
10029
],
"sales_period_info": {
"sales_period_series": 33
}
}

View File

@@ -0,0 +1,7 @@
[
{
"id": 1,
"deck_format": "5",
"end_time": "2030-06-26 19:59:59"
}
]

View File

@@ -27,7 +27,6 @@ public class GlobalsImporter
Console.WriteLine($"[GlobalsImporter] Loading captures from {capturesDir}...");
JsonElement? loadIndex = LoadCapture(capturesDir, "load-index");
JsonElement? mypageIndex = LoadCapture(capturesDir, "mypage-index");
JsonElement? deckInfo = LoadCapture(capturesDir, "deck-info");
JsonElement? packInfo = LoadCapture(capturesDir, "pack-info");
@@ -51,15 +50,6 @@ public class GlobalsImporter
total += await UpdateRotationCardSetFlags(context, loadIndex.Value);
}
if (mypageIndex.HasValue)
{
total += await ImportBanners(context, mypageIndex.Value);
total += await ImportColosseum(context, mypageIndex.Value);
total += await ImportSealed(context, mypageIndex.Value);
total += await ImportMasterPointRankingPeriod(context, mypageIndex.Value);
total += await ImportRoomTypeInSession(context, mypageIndex.Value);
}
if (deckInfo.HasValue)
{
total += await ImportDefaultDecks(context, deckInfo.Value);
@@ -521,135 +511,6 @@ public class GlobalsImporter
return updated;
}
// ---------- Mypage: Banners ----------
private async Task<int> ImportBanners(SVSimDbContext context, JsonElement mypage)
{
if (!mypage.TryGetProperty("banner", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
// Banners have no wire ID; we treat the capture as authoritative — clear and rewrite.
var existing = await context.Banners.ToListAsync();
context.Banners.RemoveRange(existing);
int created = 0;
int idx = 1;
foreach (var el in arr.EnumerateArray())
{
context.Banners.Add(new BannerEntry
{
Id = idx++,
ImageName = GetString(el, "image_name"),
Click = GetString(el, "click"),
Status = GetString(el, "status"),
ChangeTime = GetInt(el, "change_time"),
RemainingTime = GetInt(el, "remaining_time"),
ImagePaths = el.TryGetProperty("image_paths", out var ip) ? Serialize(ip) : "[]"
});
created++;
}
Console.WriteLine($"[GlobalsImporter] Banners: {(existing.Count > 0 ? $"-{existing.Count}/" : "")}+{created}");
return created;
}
// ---------- Mypage: Colosseum (singleton) ----------
private async Task<int> ImportColosseum(SVSimDbContext context, JsonElement mypage)
{
if (!mypage.TryGetProperty("colosseum_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
var existing = await context.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
var entry = existing ?? new ColosseumConfig { Id = 1 };
entry.ColosseumId = GetString(info, "colosseum_id");
entry.ColosseumName = GetString(info, "colosseum_name");
entry.CardPoolName = GetString(info, "card_pool_name");
entry.DeckFormat = GetString(info, "deck_format");
entry.StartTime = ParseWireDateTime(GetString(info, "start_time"));
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
entry.NowRound = GetString(info, "now_round");
entry.IsDisplayTips = GetString(info, "is_display_tips");
entry.TipsId = GetString(info, "tips_id");
entry.IsColosseumPeriod = GetBool(info, "is_colosseum_period");
entry.IsRoundPeriod = GetBool(info, "is_round_period");
entry.IsNormalTwoPick = GetString(info, "is_normal_two_pick");
entry.IsSpecialMode = GetString(info, "is_special_mode");
entry.IsAllCardEnabled = GetInt(info, "is_all_card_enabled");
entry.SalesPeriodInfo = info.TryGetProperty("sales_period_info", out var sp) ? Serialize(sp) : "{}";
if (existing is null) context.Colosseums.Add(entry);
Console.WriteLine($"[GlobalsImporter] Colosseum: {(existing is null ? "+1" : "~1")}");
return 1;
}
// ---------- Mypage: Sealed (singleton) ----------
private async Task<int> ImportSealed(SVSimDbContext context, JsonElement mypage)
{
if (!mypage.TryGetProperty("sealed_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
var existing = await context.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
var entry = existing ?? new SealedConfig { Id = 1 };
entry.Enable = GetInt(info, "enable");
entry.CrystalCost = GetInt(info, "crystal_cost");
entry.RupyCost = GetInt(info, "rupy_cost");
entry.TicketCost = GetInt(info, "ticket_cost");
entry.DeckUsingNumMin = GetInt(info, "deck_using_num_min");
entry.ScheduleId = GetInt(info, "schedule_id");
entry.IsJoin = GetBool(info, "is_join");
entry.IsDeckCodeMaintenance = GetBool(info, "is_deck_code_maintenance");
entry.PackInfo = info.TryGetProperty("pack_info", out var pi) ? Serialize(pi) : "[]";
entry.SalesPeriodInfo = info.TryGetProperty("sales_period_info", out var sp) ? Serialize(sp) : "{}";
if (existing is null) context.SealedSeasons.Add(entry);
Console.WriteLine($"[GlobalsImporter] Sealed: {(existing is null ? "+1" : "~1")}");
return 1;
}
// ---------- Mypage: Master Point Ranking Period ----------
private async Task<int> ImportMasterPointRankingPeriod(SVSimDbContext context, JsonElement mypage)
{
if (!mypage.TryGetProperty("master_point_ranking_period", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
int id = GetInt(info, "id");
if (id == 0) return 0;
var existing = await context.MasterPointRankingPeriods.FirstOrDefaultAsync(e => e.Id == id);
var entry = existing ?? new MasterPointRankingPeriodEntry { Id = id };
entry.PeriodNum = GetInt(info, "period_num");
entry.NecessaryScore = GetLong(info, "necessary_score");
entry.BeginTime = ParseWireDateTime(GetString(info, "begin_time"));
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
if (existing is null) context.MasterPointRankingPeriods.Add(entry);
Console.WriteLine($"[GlobalsImporter] MasterPointRankingPeriod (id={id}): {(existing is null ? "+1" : "~1")}");
return 1;
}
// ---------- Mypage: Room Type In Session (special deck formats) ----------
private async Task<int> ImportRoomTypeInSession(SVSimDbContext context, JsonElement mypage)
{
if (!mypage.TryGetProperty("room_type_in_session", out var rt) || rt.ValueKind != JsonValueKind.Object) return 0;
if (!rt.TryGetProperty("special_deck_format_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
// Same shape semantics as Banners — the wire has no stable id, treat the capture as
// authoritative and clear-and-rewrite with a synthetic ordinal.
var existing = await context.SpecialDeckFormats.ToListAsync();
context.SpecialDeckFormats.RemoveRange(existing);
int created = 0;
int idx = 1;
foreach (var el in arr.EnumerateArray())
{
context.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
{
Id = idx++,
DeckFormat = GetString(el, "deck_format"),
EndTime = ParseWireDateTime(GetString(el, "end_time"))
});
created++;
}
Console.WriteLine($"[GlobalsImporter] SpecialDeckFormats: {(existing.Count > 0 ? $"-{existing.Count}/" : "")}+{created}");
return created;
}
// ---------- Deck/info: Default Decks ----------
private async Task<int> ImportDefaultDecks(SVSimDbContext context, JsonElement deckInfo)

View File

@@ -0,0 +1,185 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using SVSim.Bootstrap.Models.Seed;
using SVSim.Database;
using SVSim.Database.Models;
namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Idempotent upsert of /mypage/index-derived globals from per-table seed files.
/// Banners and SpecialDeckFormats use CLEAR-AND-REWRITE semantics (no stable wire ID, capture is authoritative).
/// Colosseum and SealedSeason are singletons (Id=1). MasterPointRankingPeriod upserts by wire id.
/// </summary>
public class MyPageGlobalsImporter
{
public async Task<int> ImportBannersAsync(SVSimDbContext context, string seedDir)
{
var seed = SeedLoader.LoadList<BannerSeed>(Path.Combine(seedDir, "banners.json"));
if (seed.Count == 0)
{
Console.WriteLine("[MyPageGlobalsImporter] No banner seed rows; skipping.");
return 0;
}
// Clear-and-rewrite: banners have no stable wire ID, the capture is authoritative.
var existing = await context.Banners.ToListAsync();
context.Banners.RemoveRange(existing);
foreach (var s in seed)
{
context.Banners.Add(new BannerEntry
{
Id = s.Id,
ImageName = s.ImageName,
Click = s.Click,
Status = s.Status,
ChangeTime = s.ChangeTime,
RemainingTime = s.RemainingTime,
ImagePaths = s.ImagePaths.ValueKind == JsonValueKind.Undefined
? "[]"
: JsonSerializer.Serialize(s.ImagePaths),
});
}
await context.SaveChangesAsync();
Console.WriteLine($"[MyPageGlobalsImporter] Banners: -{existing.Count}/+{seed.Count}");
return seed.Count;
}
public async Task<int> ImportColosseumAsync(SVSimDbContext context, string seedDir)
{
var s = SeedLoader.LoadObject<ColosseumSeed>(Path.Combine(seedDir, "colosseum.json"));
if (s is null)
{
Console.WriteLine("[MyPageGlobalsImporter] No colosseum seed; skipping.");
return 0;
}
var existing = await context.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
var entry = existing ?? new ColosseumConfig { Id = 1 };
entry.ColosseumId = s.ColosseumId;
entry.ColosseumName = s.ColosseumName;
entry.CardPoolName = s.CardPoolName;
entry.DeckFormat = s.DeckFormat;
entry.StartTime = ImporterBase.ParseWireDateTime(s.StartTime);
entry.EndTime = ImporterBase.ParseWireDateTime(s.EndTime);
entry.NowRound = s.NowRound;
entry.IsDisplayTips = s.IsDisplayTips;
entry.TipsId = s.TipsId;
entry.IsColosseumPeriod = s.IsColosseumPeriod;
entry.IsRoundPeriod = s.IsRoundPeriod;
entry.IsNormalTwoPick = s.IsNormalTwoPick;
entry.IsSpecialMode = s.IsSpecialMode;
entry.IsAllCardEnabled = s.IsAllCardEnabled;
entry.SalesPeriodInfo = s.SalesPeriodInfo.ValueKind == JsonValueKind.Undefined
? "{}"
: JsonSerializer.Serialize(s.SalesPeriodInfo);
if (existing is null) context.Colosseums.Add(entry);
await context.SaveChangesAsync();
Console.WriteLine($"[MyPageGlobalsImporter] Colosseum: {(existing is null ? "+1" : "~1")}");
return 1;
}
public async Task<int> ImportSealedAsync(SVSimDbContext context, string seedDir)
{
var s = SeedLoader.LoadObject<SealedSeasonSeed>(Path.Combine(seedDir, "sealed-season.json"));
if (s is null)
{
Console.WriteLine("[MyPageGlobalsImporter] No sealed-season seed; skipping.");
return 0;
}
var existing = await context.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
var entry = existing ?? new SealedConfig { Id = 1 };
entry.Enable = s.Enable;
entry.CrystalCost = s.CrystalCost;
entry.RupyCost = s.RupyCost;
entry.TicketCost = s.TicketCost;
entry.DeckUsingNumMin = s.DeckUsingNumMin;
entry.ScheduleId = s.ScheduleId;
entry.IsJoin = s.IsJoin;
entry.IsDeckCodeMaintenance = s.IsDeckCodeMaintenance;
entry.PackInfo = s.PackInfo.ValueKind == JsonValueKind.Undefined
? "[]"
: JsonSerializer.Serialize(s.PackInfo);
entry.SalesPeriodInfo = s.SalesPeriodInfo.ValueKind == JsonValueKind.Undefined
? "{}"
: JsonSerializer.Serialize(s.SalesPeriodInfo);
if (existing is null) context.SealedSeasons.Add(entry);
await context.SaveChangesAsync();
Console.WriteLine($"[MyPageGlobalsImporter] SealedSeason: {(existing is null ? "+1" : "~1")}");
return 1;
}
public async Task<int> ImportMasterPointRankingPeriodAsync(SVSimDbContext context, string seedDir)
{
var seed = SeedLoader.LoadList<MasterPointRankingPeriodSeed>(
Path.Combine(seedDir, "master-point-ranking-periods.json"));
if (seed.Count == 0)
{
Console.WriteLine("[MyPageGlobalsImporter] No master-point-ranking-period seed rows; skipping.");
return 0;
}
var existing = await context.MasterPointRankingPeriods.ToDictionaryAsync(e => e.Id);
int created = 0, updated = 0;
foreach (var s in seed)
{
if (s.Id == 0) continue;
var entry = existing.TryGetValue(s.Id, out var ex)
? ex : new MasterPointRankingPeriodEntry { Id = s.Id };
entry.PeriodNum = s.PeriodNum;
entry.NecessaryScore = s.NecessaryScore;
entry.BeginTime = ImporterBase.ParseWireDateTime(s.BeginTime);
entry.EndTime = ImporterBase.ParseWireDateTime(s.EndTime);
if (ex is null)
{
context.MasterPointRankingPeriods.Add(entry);
existing[s.Id] = entry;
created++;
}
else updated++;
}
await context.SaveChangesAsync();
Console.WriteLine($"[MyPageGlobalsImporter] MasterPointRankingPeriod: +{created}/~{updated}");
return created + updated;
}
public async Task<int> ImportSpecialDeckFormatsAsync(SVSimDbContext context, string seedDir)
{
var seed = SeedLoader.LoadList<SpecialDeckFormatSeed>(
Path.Combine(seedDir, "special-deck-formats.json"));
if (seed.Count == 0)
{
Console.WriteLine("[MyPageGlobalsImporter] No special-deck-format seed rows; skipping.");
return 0;
}
// Clear-and-rewrite: same semantics as banners — no stable wire ID, capture is authoritative.
var existing = await context.SpecialDeckFormats.ToListAsync();
context.SpecialDeckFormats.RemoveRange(existing);
foreach (var s in seed)
{
context.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
{
Id = s.Id,
DeckFormat = s.DeckFormat,
EndTime = ImporterBase.ParseWireDateTime(s.EndTime),
});
}
await context.SaveChangesAsync();
Console.WriteLine($"[MyPageGlobalsImporter] SpecialDeckFormats: -{existing.Count}/+{seed.Count}");
return seed.Count;
}
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SVSim.Bootstrap.Models.Seed;
public sealed class BannerSeed
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("image_name")] public string ImageName { get; set; } = "";
[JsonPropertyName("click")] public string Click { get; set; } = "";
[JsonPropertyName("status")] public string Status { get; set; } = "";
[JsonPropertyName("change_time")] public int ChangeTime { get; set; }
[JsonPropertyName("remaining_time")] public int RemainingTime { get; set; }
[JsonPropertyName("image_paths")] public JsonElement ImagePaths { get; set; }
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SVSim.Bootstrap.Models.Seed;
public sealed class ColosseumSeed
{
[JsonPropertyName("id")] public int Id { get; set; } = 1;
[JsonPropertyName("colosseum_id")] public string ColosseumId { get; set; } = "";
[JsonPropertyName("colosseum_name")] public string ColosseumName { get; set; } = "";
[JsonPropertyName("card_pool_name")] public string CardPoolName { get; set; } = "";
[JsonPropertyName("deck_format")] public string DeckFormat { get; set; } = "";
[JsonPropertyName("start_time")] public string StartTime { get; set; } = "";
[JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
[JsonPropertyName("now_round")] public string NowRound { get; set; } = "";
[JsonPropertyName("is_display_tips")] public string IsDisplayTips { get; set; } = "";
[JsonPropertyName("tips_id")] public string TipsId { get; set; } = "";
[JsonPropertyName("is_colosseum_period")] public bool IsColosseumPeriod { get; set; }
[JsonPropertyName("is_round_period")] public bool IsRoundPeriod { get; set; }
[JsonPropertyName("is_normal_two_pick")] public string IsNormalTwoPick { get; set; } = "";
[JsonPropertyName("is_special_mode")] public string IsSpecialMode { get; set; } = "";
[JsonPropertyName("is_all_card_enabled")] public int IsAllCardEnabled { get; set; }
[JsonPropertyName("sales_period_info")] public JsonElement SalesPeriodInfo { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SVSim.Bootstrap.Models.Seed;
public sealed class MasterPointRankingPeriodSeed
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("period_num")] public int PeriodNum { get; set; }
[JsonPropertyName("necessary_score")] public long NecessaryScore { get; set; }
[JsonPropertyName("begin_time")] public string BeginTime { get; set; } = "";
[JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SVSim.Bootstrap.Models.Seed;
public sealed class SealedSeasonSeed
{
[JsonPropertyName("id")] public int Id { get; set; } = 1;
[JsonPropertyName("enable")] public int Enable { get; set; }
[JsonPropertyName("crystal_cost")] public int CrystalCost { get; set; }
[JsonPropertyName("rupy_cost")] public int RupyCost { get; set; }
[JsonPropertyName("ticket_cost")] public int TicketCost { get; set; }
[JsonPropertyName("deck_using_num_min")] public int DeckUsingNumMin { get; set; }
[JsonPropertyName("schedule_id")] public int ScheduleId { get; set; }
[JsonPropertyName("is_join")] public bool IsJoin { get; set; }
[JsonPropertyName("is_deck_code_maintenance")] public bool IsDeckCodeMaintenance { get; set; }
[JsonPropertyName("pack_info")] public JsonElement PackInfo { get; set; }
[JsonPropertyName("sales_period_info")] public JsonElement SalesPeriodInfo { get; set; }
}

View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace SVSim.Bootstrap.Models.Seed;
public sealed class SpecialDeckFormatSeed
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("deck_format")] public string DeckFormat { get; set; } = "";
[JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
}

View File

@@ -83,6 +83,13 @@ public static class Program
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);
// 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).

View File

@@ -0,0 +1,187 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Bootstrap.Importers;
using SVSim.Database;
using SVSim.Database.Models;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Importers;
public class MyPageGlobalsImporterTests
{
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
[Test]
public async Task Imports_banners_from_seed_file()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var importer = new MyPageGlobalsImporter();
await importer.ImportBannersAsync(db, SeedDir);
int count = await db.Banners.CountAsync();
Assert.That(count, Is.GreaterThan(0), "seed must contain banners");
}
[Test]
public async Task Imports_colosseum_singleton()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var importer = new MyPageGlobalsImporter();
await importer.ImportColosseumAsync(db, SeedDir);
var row = await db.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
Assert.That(row, Is.Not.Null);
Assert.That(row!.Id, Is.EqualTo(1));
Assert.That(row.ColosseumId, Is.Not.Empty);
}
[Test]
public async Task Imports_sealed_singleton()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var importer = new MyPageGlobalsImporter();
await importer.ImportSealedAsync(db, SeedDir);
var row = await db.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
Assert.That(row, Is.Not.Null);
Assert.That(row!.Id, Is.EqualTo(1));
// pack_info is a JSON array column, must be non-empty in the captured seed.
Assert.That(row.PackInfo, Does.StartWith("["));
}
[Test]
public async Task Imports_master_point_ranking_period()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var importer = new MyPageGlobalsImporter();
await importer.ImportMasterPointRankingPeriodAsync(db, SeedDir);
int count = await db.MasterPointRankingPeriods.CountAsync();
Assert.That(count, Is.GreaterThan(0), "seed must contain at least one ranking period");
}
[Test]
public async Task Imports_special_deck_formats()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var importer = new MyPageGlobalsImporter();
await importer.ImportSpecialDeckFormatsAsync(db, SeedDir);
int count = await db.SpecialDeckFormats.CountAsync();
Assert.That(count, Is.GreaterThan(0), "seed must contain special deck formats");
}
[Test]
public async Task Banners_are_clear_and_rewrite()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
// Pre-seed a legacy banner with Id outside the seed range — the importer must wipe it.
db.Banners.Add(new BannerEntry
{
Id = 999,
ImageName = "legacy_banner",
Click = "legacy",
Status = "9",
ChangeTime = 0,
RemainingTime = 0,
ImagePaths = "[]",
});
await db.SaveChangesAsync();
var importer = new MyPageGlobalsImporter();
await importer.ImportBannersAsync(db, SeedDir);
var stale = await db.Banners.FindAsync(999);
Assert.That(stale, Is.Null,
"ImportBannersAsync must clear-and-rewrite — pre-existing legacy rows must be removed");
}
[Test]
public async Task Special_deck_formats_are_clear_and_rewrite()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
{
Id = 999,
DeckFormat = "99",
EndTime = DateTime.UtcNow,
});
await db.SaveChangesAsync();
var importer = new MyPageGlobalsImporter();
await importer.ImportSpecialDeckFormatsAsync(db, SeedDir);
var stale = await db.SpecialDeckFormats.FindAsync(999);
Assert.That(stale, Is.Null,
"ImportSpecialDeckFormatsAsync must clear-and-rewrite — pre-existing legacy rows must be removed");
}
[Test]
public async Task Singletons_are_idempotent_on_rerun()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var importer = new MyPageGlobalsImporter();
await importer.ImportColosseumAsync(db, SeedDir);
await importer.ImportSealedAsync(db, SeedDir);
await importer.ImportColosseumAsync(db, SeedDir);
await importer.ImportSealedAsync(db, SeedDir);
Assert.That(await db.Colosseums.CountAsync(), Is.EqualTo(1),
"Colosseum singleton must remain a single row on re-run");
Assert.That(await db.SealedSeasons.CountAsync(), Is.EqualTo(1),
"SealedSeason singleton must remain a single row on re-run");
}
[Test]
public async Task Master_point_ranking_period_leaves_existing_rows_untouched()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
const int legacyId = 88888;
db.MasterPointRankingPeriods.Add(new MasterPointRankingPeriodEntry
{
Id = legacyId,
PeriodNum = 87,
NecessaryScore = 12345,
BeginTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc),
EndTime = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
});
await db.SaveChangesAsync();
var importer = new MyPageGlobalsImporter();
await importer.ImportMasterPointRankingPeriodAsync(db, SeedDir);
var legacy = await db.MasterPointRankingPeriods.FindAsync(legacyId);
Assert.That(legacy, Is.Not.Null,
"MasterPointRankingPeriod upserts by id — legacy rows not in seed must survive");
Assert.That(legacy!.PeriodNum, Is.EqualTo(87));
Assert.That(legacy.NecessaryScore, Is.EqualTo(12345));
}
}

View File

@@ -198,6 +198,13 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
await puzzleImporter.ImportGroupsAsync(ctx, seedDir);
await puzzleImporter.ImportPuzzlesAsync(ctx, seedDir);
await puzzleImporter.ImportMissionsAsync(ctx, seedDir);
var mypage = new MyPageGlobalsImporter();
await mypage.ImportBannersAsync(ctx, seedDir);
await mypage.ImportColosseumAsync(ctx, seedDir);
await mypage.ImportSealedAsync(ctx, seedDir);
await mypage.ImportMasterPointRankingPeriodAsync(ctx, seedDir);
await mypage.ImportSpecialDeckFormatsAsync(ctx, seedDir);
}
/// <summary>Convenience: bake the X-Test-Viewer-Id header into a fresh client.</summary>