diff --git a/SVSim.Bootstrap/Importers/ArenaSeasonImporter.cs b/SVSim.Bootstrap/Importers/ArenaSeasonImporter.cs
new file mode 100644
index 0000000..67d1e6c
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/ArenaSeasonImporter.cs
@@ -0,0 +1,37 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Singleton upsert (Id=1) of the active Take Two arena season config from
+/// seeds/arena-season.json. format_info is preserved verbatim as a jsonb blob.
+///
+public class ArenaSeasonImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var s = SeedLoader.LoadObject(Path.Combine(seedDir, "arena-season.json"));
+ if (s is null) return 0;
+
+ var existing = await context.ArenaSeasons.FirstOrDefaultAsync(e => e.Id == 1);
+ var entry = existing ?? new ArenaSeasonConfig { Id = 1 };
+ entry.Mode = s.Mode;
+ entry.Enable = s.Enable;
+ entry.Cost = s.Cost;
+ entry.RupyCost = s.RupyCost;
+ entry.TicketCost = s.TicketCost;
+ entry.IsJoin = s.IsJoin;
+ entry.FormatInfo = s.FormatInfo.ValueKind == JsonValueKind.Undefined
+ ? "{}"
+ : JsonSerializer.Serialize(s.FormatInfo);
+ if (existing is null) context.ArenaSeasons.Add(entry);
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[ArenaSeasonImporter] {(existing is null ? "+1" : "~1")}");
+ return 1;
+ }
+}
diff --git a/SVSim.Bootstrap/Importers/AvatarAbilityImporter.cs b/SVSim.Bootstrap/Importers/AvatarAbilityImporter.cs
new file mode 100644
index 0000000..3038c70
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/AvatarAbilityImporter.cs
@@ -0,0 +1,41 @@
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Idempotent upsert of Avatar (Hero) ability rows from seeds/avatar-abilities.json.
+/// Keyed by leader_skin_id. Ability / passive-ability DSL strings are preserved verbatim.
+///
+public class AvatarAbilityImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var seed = SeedLoader.LoadList(Path.Combine(seedDir, "avatar-abilities.json"));
+ if (seed.Count == 0) return 0;
+
+ var existing = await context.AvatarAbilities.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 AvatarAbilityEntry { Id = s.Id };
+ entry.BattleStartFirstPlayerTurnBp = s.BattleStartFirstPlayerTurnBp;
+ entry.BattleStartSecondPlayerTurnBp = s.BattleStartSecondPlayerTurnBp;
+ entry.BattleStartMaxLife = s.BattleStartMaxLife;
+ entry.AbilityCost = s.AbilityCost;
+ entry.Ability = s.Ability;
+ entry.PassiveAbility = s.PassiveAbility;
+ entry.AbilityDesc = s.AbilityDesc;
+ entry.PassiveAbilityDesc = s.PassiveAbilityDesc;
+ if (ex is null) { context.AvatarAbilities.Add(entry); existing[s.Id] = entry; created++; }
+ else updated++;
+ }
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[AvatarAbilityImporter] +{created}/~{updated}");
+ return created + updated;
+ }
+}
diff --git a/SVSim.Bootstrap/Importers/BattlePassImporter.cs b/SVSim.Bootstrap/Importers/BattlePassImporter.cs
new file mode 100644
index 0000000..b29d091
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/BattlePassImporter.cs
@@ -0,0 +1,37 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Idempotent upsert of battle-pass level rows from seeds/battle-pass-levels.json.
+/// Per-level reward_data blob preserved verbatim (shape varies per level).
+///
+public class BattlePassImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var seed = SeedLoader.LoadList(Path.Combine(seedDir, "battle-pass-levels.json"));
+ if (seed.Count == 0) return 0;
+
+ var existing = await context.BattlePassLevels.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 BattlePassLevelEntry { Id = s.Id };
+ entry.RewardData = s.RewardData.ValueKind == JsonValueKind.Undefined
+ ? "{}"
+ : JsonSerializer.Serialize(s.RewardData);
+ if (ex is null) { context.BattlePassLevels.Add(entry); existing[s.Id] = entry; created++; }
+ else updated++;
+ }
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[BattlePassImporter] +{created}/~{updated}");
+ return created + updated;
+ }
+}
diff --git a/SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs b/SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs
new file mode 100644
index 0000000..1c158f1
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/DailyLoginBonusImporter.cs
@@ -0,0 +1,37 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Idempotent upsert of daily-login-bonus campaign rows from seeds/daily-login-bonus.json.
+/// bonus_data array preserved verbatim — prod observed empty arrays outside active events.
+///
+public class DailyLoginBonusImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var seed = SeedLoader.LoadList(Path.Combine(seedDir, "daily-login-bonus.json"));
+ if (seed.Count == 0) return 0;
+
+ var existing = await context.DailyLoginBonuses.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 DailyLoginBonusEntry { Id = s.Id };
+ entry.BonusData = s.BonusData.ValueKind == JsonValueKind.Undefined
+ ? "[]"
+ : JsonSerializer.Serialize(s.BonusData);
+ if (ex is null) { context.DailyLoginBonuses.Add(entry); existing[s.Id] = entry; created++; }
+ else updated++;
+ }
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[DailyLoginBonusImporter] +{created}/~{updated}");
+ return created + updated;
+ }
+}
diff --git a/SVSim.Bootstrap/Importers/MyRotationImporter.cs b/SVSim.Bootstrap/Importers/MyRotationImporter.cs
new file mode 100644
index 0000000..0484a8a
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/MyRotationImporter.cs
@@ -0,0 +1,53 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Idempotent upsert of MyRotation reference data: settings (per rotation_id) +
+/// abilities (per ability_id). Seeds come from seeds/my-rotation-settings.json and
+/// seeds/my-rotation-abilities.json; the extractor pre-joins the original wire's three
+/// dicts (setting, reprinted, restricted) on rotation_id, so the importer just iterates.
+///
+public class MyRotationImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var settings = SeedLoader.LoadList(Path.Combine(seedDir, "my-rotation-settings.json"));
+ var abilities = SeedLoader.LoadList(Path.Combine(seedDir, "my-rotation-abilities.json"));
+
+ if (settings.Count == 0 && abilities.Count == 0) return 0;
+
+ int sCreated = 0, sUpdated = 0;
+ var existingSettings = await context.MyRotationSettings.ToDictionaryAsync(e => e.Id);
+ foreach (var s in settings)
+ {
+ if (s.Id == 0) continue;
+ var entry = existingSettings.TryGetValue(s.Id, out var ex) ? ex : new MyRotationSettingEntry { Id = s.Id };
+ entry.CardSetIdsCsv = s.CardSetIdsCsv;
+ entry.AbilitiesCsv = s.AbilitiesCsv;
+ entry.ReprintedCardIds = s.ReprintedCardIds;
+ entry.RestrictedCardIds = s.RestrictedCardIds;
+ if (ex is null) { context.MyRotationSettings.Add(entry); existingSettings[s.Id] = entry; sCreated++; }
+ else sUpdated++;
+ }
+
+ int aCreated = 0, aUpdated = 0;
+ var existingAbilities = await context.MyRotationAbilities.ToDictionaryAsync(e => e.Id);
+ foreach (var s in abilities)
+ {
+ if (s.Id == 0) continue;
+ var entry = existingAbilities.TryGetValue(s.Id, out var ex) ? ex : new MyRotationAbilityEntry { Id = s.Id };
+ entry.Data = JsonSerializer.Serialize(s.Data);
+ if (ex is null) { context.MyRotationAbilities.Add(entry); existingAbilities[s.Id] = entry; aCreated++; }
+ else aUpdated++;
+ }
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[MyRotationImporter] settings +{sCreated}/~{sUpdated}, abilities +{aCreated}/~{aUpdated}");
+ return sCreated + sUpdated + aCreated + aUpdated;
+ }
+}
diff --git a/SVSim.Bootstrap/Importers/PreReleaseInfoImporter.cs b/SVSim.Bootstrap/Importers/PreReleaseInfoImporter.cs
new file mode 100644
index 0000000..0ca4168
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/PreReleaseInfoImporter.cs
@@ -0,0 +1,46 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+using static SVSim.Bootstrap.Importers.ImporterBase;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Singleton upsert (Id=1) of the pre-release window from seeds/pre-release-info.json.
+/// Card-id list / dict blobs are preserved verbatim into their jsonb columns; date strings go
+/// through .
+///
+public class PreReleaseInfoImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var s = SeedLoader.LoadObject(Path.Combine(seedDir, "pre-release-info.json"));
+ if (s is null) return 0;
+
+ var existing = await context.PreReleaseInfos.FirstOrDefaultAsync(e => e.Id == 1);
+ var entry = existing ?? new PreReleaseInfo { Id = 1 };
+ entry.PreReleaseId = s.PreReleaseId;
+ entry.NextCardSetId = s.NextCardSetId;
+ entry.StartTime = ParseWireDateTime(s.StartTime);
+ entry.EndTime = ParseWireDateTime(s.EndTime);
+ entry.DisplayEndTime = ParseWireDateTime(s.DisplayEndTime);
+ entry.FreeMatchStartTime = ParseWireDateTime(s.FreeMatchStartTime);
+ entry.CardMasterId = s.CardMasterId;
+ entry.DefaultCardMasterId = s.DefaultCardMasterId;
+ entry.PreReleaseCardMasterId = s.PreReleaseCardMasterId;
+ entry.IsPreRotationFreeMatchTerm = s.IsPreRotationFreeMatchTerm;
+ entry.RotationCardSetIdList = s.RotationCardSetIdList.ValueKind == JsonValueKind.Undefined
+ ? "[]" : JsonSerializer.Serialize(s.RotationCardSetIdList);
+ entry.ReprintedBaseCardIds = s.ReprintedBaseCardIds.ValueKind == JsonValueKind.Undefined
+ ? "{}" : JsonSerializer.Serialize(s.ReprintedBaseCardIds);
+ entry.LatestReprintedBaseCardIds = s.LatestReprintedBaseCardIds.ValueKind == JsonValueKind.Undefined
+ ? "{}" : JsonSerializer.Serialize(s.LatestReprintedBaseCardIds);
+ if (existing is null) context.PreReleaseInfos.Add(entry);
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[PreReleaseInfoImporter] {(existing is null ? "+1" : "~1")}");
+ return 1;
+ }
+}
diff --git a/SVSim.Bootstrap/Importers/RotationConfigImporter.cs b/SVSim.Bootstrap/Importers/RotationConfigImporter.cs
new file mode 100644
index 0000000..21bf238
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/RotationConfigImporter.cs
@@ -0,0 +1,102 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Models;
+using SVSim.Database.Models.Config;
+using static SVSim.Bootstrap.Importers.ImporterBase;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Writes three GameConfigSection rows from the load-index seed split:
+/// seeds/rotation-config.json → Rotation, seeds/challenge-config.json → Challenge,
+/// seeds/my-rotation-schedule.json → MyRotationSchedule. Atomic section pattern: read the
+/// existing section row (or shipped defaults), mutate the deserialized POCO, write back to
+/// ValueJson. Re-runnable; rows missing from the seed leave the section row untouched.
+///
+public class RotationConfigImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ int touched = 0;
+
+ var rot = SeedLoader.LoadObject(Path.Combine(seedDir, "rotation-config.json"));
+ if (rot is not null)
+ {
+ await UpsertSection(context, RotationConfig.ShippedDefaults, c =>
+ {
+ c.TsRotationId = rot.TsRotationId;
+ c.IsBattlePassPeriod = rot.IsBattlePassPeriod;
+ c.IsBeginnerMission = rot.IsBeginnerMission;
+ c.CardSetIdForResourceDlView = rot.CardSetIdForResourceDlView;
+ });
+ touched++;
+ }
+
+ var cc = SeedLoader.LoadObject(Path.Combine(seedDir, "challenge-config.json"));
+ if (cc is not null)
+ {
+ await UpsertSection(context, ChallengeConfig.ShippedDefaults, c =>
+ {
+ c.UseTwoPickPremiumCard = cc.UseTwoPickPremiumCard;
+ c.TwoPickSleeveId = cc.TwoPickSleeveId;
+ });
+ touched++;
+ }
+
+ var schedule = SeedLoader.LoadObject(Path.Combine(seedDir, "my-rotation-schedule.json"));
+ if (schedule?.Gathering is not null && schedule.FreeBattle is not null)
+ {
+ var gBegin = ParseWireDateTime(schedule.Gathering.Begin);
+ var gEnd = ParseWireDateTime(schedule.Gathering.End);
+ var fBegin = ParseWireDateTime(schedule.FreeBattle.Begin);
+ var fEnd = ParseWireDateTime(schedule.FreeBattle.End);
+ // Only commit when both windows parsed to real DateTimes — a malformed/0001 value
+ // would silently lock the MyRotation feature off (the original bug the section fixed).
+ if (gBegin != DateTime.MinValue && gEnd != DateTime.MinValue
+ && fBegin != DateTime.MinValue && fEnd != DateTime.MinValue)
+ {
+ await UpsertSection(context, MyRotationScheduleConfig.ShippedDefaults, c =>
+ {
+ c.Gathering = new ScheduleWindow { Begin = gBegin, End = gEnd };
+ c.FreeBattle = new ScheduleWindow { Begin = fBegin, End = fEnd };
+ });
+ touched++;
+ }
+ else
+ {
+ Console.Error.WriteLine("[RotationConfigImporter] my-rotation-schedule.json windows malformed — keeping existing/shipped MyRotationSchedule.");
+ }
+ }
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[RotationConfigImporter] sections={touched}");
+ return touched;
+ }
+
+ // Verbatim copy of GlobalsImporter.UpsertSection. Kept private-static here so this
+ // importer can stand alone after Stage 9C strips the GlobalsImporter copy.
+ private static async Task UpsertSection(SVSimDbContext context, Func shippedDefaults, Action mutate)
+ where T : class, new()
+ {
+ var sectionName = typeof(T).GetCustomAttributes(typeof(ConfigSectionAttribute), inherit: false)
+ .Cast().FirstOrDefault()?.Name
+ ?? throw new InvalidOperationException($"{typeof(T).Name} is missing [ConfigSection].");
+
+ var row = await context.GameConfigs.FirstOrDefaultAsync(s => s.SectionName == sectionName);
+ T value;
+ if (row is null)
+ {
+ value = shippedDefaults();
+ row = new GameConfigSection { SectionName = sectionName };
+ context.GameConfigs.Add(row);
+ }
+ else
+ {
+ value = JsonSerializer.Deserialize(row.ValueJson) ?? shippedDefaults();
+ }
+ mutate(value);
+ row.ValueJson = JsonSerializer.Serialize(value);
+ }
+}
diff --git a/SVSim.Bootstrap/Models/Seed/ArenaSeasonSeed.cs b/SVSim.Bootstrap/Models/Seed/ArenaSeasonSeed.cs
new file mode 100644
index 0000000..be3080b
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/ArenaSeasonSeed.cs
@@ -0,0 +1,20 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+///
+/// Mirrors seeds/arena-season.json. Singleton (id=1) holding the Take Two arena season.
+/// format_info is a nested JSON object stored verbatim as the entity's FormatInfo jsonb.
+///
+public sealed class ArenaSeasonSeed
+{
+ [JsonPropertyName("id")] public int Id { get; set; }
+ [JsonPropertyName("mode")] public int Mode { get; set; }
+ [JsonPropertyName("enable")] public int Enable { get; set; }
+ [JsonPropertyName("cost")] public ulong Cost { get; set; }
+ [JsonPropertyName("rupy_cost")] public ulong RupyCost { get; set; }
+ [JsonPropertyName("ticket_cost")] public int TicketCost { get; set; }
+ [JsonPropertyName("is_join")] public bool IsJoin { get; set; }
+ [JsonPropertyName("format_info")] public JsonElement FormatInfo { get; set; }
+}
diff --git a/SVSim.Bootstrap/Models/Seed/AvatarAbilitySeed.cs b/SVSim.Bootstrap/Models/Seed/AvatarAbilitySeed.cs
new file mode 100644
index 0000000..95bb220
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/AvatarAbilitySeed.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+/// Mirrors seeds/avatar-abilities.json. One row per leader_skin_id.
+public sealed class AvatarAbilitySeed
+{
+ [JsonPropertyName("id")] public int Id { get; set; }
+ [JsonPropertyName("battle_start_first_player_turn_bp")] public int BattleStartFirstPlayerTurnBp { get; set; }
+ [JsonPropertyName("battle_start_second_player_turn_bp")] public int BattleStartSecondPlayerTurnBp { get; set; }
+ [JsonPropertyName("battle_start_max_life")] public int BattleStartMaxLife { get; set; }
+ [JsonPropertyName("ability_cost")] public string AbilityCost { get; set; } = "";
+ [JsonPropertyName("ability")] public string Ability { get; set; } = "";
+ [JsonPropertyName("passive_ability")] public string PassiveAbility { get; set; } = "";
+ [JsonPropertyName("ability_desc")] public string AbilityDesc { get; set; } = "";
+ [JsonPropertyName("passive_ability_desc")] public string PassiveAbilityDesc { get; set; } = "";
+}
diff --git a/SVSim.Bootstrap/Models/Seed/BattlePassLevelSeed.cs b/SVSim.Bootstrap/Models/Seed/BattlePassLevelSeed.cs
new file mode 100644
index 0000000..0869c8d
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/BattlePassLevelSeed.cs
@@ -0,0 +1,11 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+/// Mirrors seeds/battle-pass-levels.json. reward_data preserved verbatim.
+public sealed class BattlePassLevelSeed
+{
+ [JsonPropertyName("id")] public int Id { get; set; }
+ [JsonPropertyName("reward_data")] public JsonElement RewardData { get; set; }
+}
diff --git a/SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs b/SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs
new file mode 100644
index 0000000..e64ed76
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/DailyLoginBonusSeed.cs
@@ -0,0 +1,11 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+/// Mirrors seeds/daily-login-bonus.json. bonus_data preserved verbatim.
+public sealed class DailyLoginBonusSeed
+{
+ [JsonPropertyName("id")] public int Id { get; set; }
+ [JsonPropertyName("bonus_data")] public JsonElement BonusData { get; set; }
+}
diff --git a/SVSim.Bootstrap/Models/Seed/MyRotationAbilitySeed.cs b/SVSim.Bootstrap/Models/Seed/MyRotationAbilitySeed.cs
new file mode 100644
index 0000000..739a937
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/MyRotationAbilitySeed.cs
@@ -0,0 +1,11 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+/// Mirrors seeds/my-rotation-abilities.json. data is preserved as raw JSON.
+public sealed class MyRotationAbilitySeed
+{
+ [JsonPropertyName("id")] public int Id { get; set; }
+ [JsonPropertyName("data")] public JsonElement Data { get; set; }
+}
diff --git a/SVSim.Bootstrap/Models/Seed/MyRotationSettingSeed.cs b/SVSim.Bootstrap/Models/Seed/MyRotationSettingSeed.cs
new file mode 100644
index 0000000..350c392
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/MyRotationSettingSeed.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+///
+/// Mirrors seeds/my-rotation-settings.json. The extractor pre-joins
+/// my_rotation_info.{setting, reprinted_base_card_ids, restricted_base_card_id_list} on
+/// rotation_id into one flat list. reprinted_card_ids and restricted_card_ids are
+/// pre-serialized JSON strings (verbatim from the wire) — the importer stores them verbatim.
+///
+public sealed class MyRotationSettingSeed
+{
+ [JsonPropertyName("id")] public int Id { get; set; }
+ [JsonPropertyName("card_set_ids_csv")] public string CardSetIdsCsv { get; set; } = "";
+ [JsonPropertyName("abilities_csv")] public string AbilitiesCsv { get; set; } = "";
+ [JsonPropertyName("reprinted_card_ids")] public string ReprintedCardIds { get; set; } = "[]";
+ [JsonPropertyName("restricted_card_ids")] public string RestrictedCardIds { get; set; } = "[]";
+}
diff --git a/SVSim.Bootstrap/Models/Seed/PreReleaseInfoSeed.cs b/SVSim.Bootstrap/Models/Seed/PreReleaseInfoSeed.cs
new file mode 100644
index 0000000..29c8f49
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/PreReleaseInfoSeed.cs
@@ -0,0 +1,25 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+///
+/// Mirrors seeds/pre-release-info.json. Singleton (id=1). Card-id lists are kept as raw
+/// JSON elements so they round-trip verbatim into the entity's jsonb columns.
+///
+public sealed class PreReleaseInfoSeed
+{
+ [JsonPropertyName("pre_release_id")] public string PreReleaseId { get; set; } = "";
+ [JsonPropertyName("next_card_set_id")] public string NextCardSetId { get; set; } = "";
+ [JsonPropertyName("start_time")] public string StartTime { get; set; } = "";
+ [JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
+ [JsonPropertyName("display_end_time")] public string DisplayEndTime { get; set; } = "";
+ [JsonPropertyName("free_match_start_time")] public string FreeMatchStartTime { get; set; } = "";
+ [JsonPropertyName("card_master_id")] public int CardMasterId { get; set; }
+ [JsonPropertyName("default_card_master_id")] public string DefaultCardMasterId { get; set; } = "";
+ [JsonPropertyName("pre_release_card_master_id")] public string PreReleaseCardMasterId { get; set; } = "";
+ [JsonPropertyName("is_pre_rotation_free_match_term")] public bool IsPreRotationFreeMatchTerm { get; set; }
+ [JsonPropertyName("rotation_card_set_id_list")] public JsonElement RotationCardSetIdList { get; set; }
+ [JsonPropertyName("reprinted_base_card_ids")] public JsonElement ReprintedBaseCardIds { get; set; }
+ [JsonPropertyName("latest_reprinted_base_card_ids")] public JsonElement LatestReprintedBaseCardIds { get; set; }
+}
diff --git a/SVSim.Bootstrap/Models/Seed/RotationConfigSeed.cs b/SVSim.Bootstrap/Models/Seed/RotationConfigSeed.cs
new file mode 100644
index 0000000..f246499
--- /dev/null
+++ b/SVSim.Bootstrap/Models/Seed/RotationConfigSeed.cs
@@ -0,0 +1,41 @@
+using System.Text.Json.Serialization;
+
+namespace SVSim.Bootstrap.Models.Seed;
+
+///
+/// Mirrors seeds/rotation-config.json. Drives the Rotation GameConfigSection.
+/// Note: rotation_card_set_ids is the rotation CardSet flag list — consumed by
+/// RotationFlagUpdater in Stage 9C, not by RotationConfigImporter.
+///
+public sealed class RotationConfigSeed
+{
+ [JsonPropertyName("ts_rotation_id")] public string TsRotationId { get; set; } = "";
+ [JsonPropertyName("is_battle_pass_period")] public bool IsBattlePassPeriod { get; set; }
+ [JsonPropertyName("is_beginner_mission")] public bool IsBeginnerMission { get; set; }
+ [JsonPropertyName("card_set_id_for_resource_dl_view")] public int CardSetIdForResourceDlView { get; set; }
+ [JsonPropertyName("rotation_card_set_ids")] public List RotationCardSetIds { get; set; } = new();
+}
+
+/// Mirrors seeds/challenge-config.json. Drives the Challenge GameConfigSection.
+public sealed class ChallengeConfigSeed
+{
+ [JsonPropertyName("use_two_pick_premium_card")] public bool UseTwoPickPremiumCard { get; set; }
+ [JsonPropertyName("two_pick_sleeve_id")] public long TwoPickSleeveId { get; set; }
+}
+
+///
+/// Mirrors seeds/my-rotation-schedule.json. Drives the MyRotationSchedule
+/// GameConfigSection. The extractor pre-joins gathering and free_battle
+/// from my_rotation_info.schedules into two top-level fields.
+///
+public sealed class MyRotationScheduleSeed
+{
+ [JsonPropertyName("gathering")] public ScheduleWindowSeed? Gathering { get; set; }
+ [JsonPropertyName("free_battle")] public ScheduleWindowSeed? FreeBattle { get; set; }
+}
+
+public sealed class ScheduleWindowSeed
+{
+ [JsonPropertyName("begin")] public string Begin { get; set; } = "";
+ [JsonPropertyName("end")] public string End { get; set; } = "";
+}
diff --git a/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs b/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs
new file mode 100644
index 0000000..7e3a4ad
--- /dev/null
+++ b/SVSim.UnitTests/Importers/LoadIndexImporterTests.cs
@@ -0,0 +1,118 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using SVSim.Bootstrap.Importers;
+using SVSim.Database;
+using SVSim.UnitTests.Infrastructure;
+
+namespace SVSim.UnitTests.Importers;
+
+///
+/// Happy-path coverage for the 7 load-index importer classes introduced in Stage 9B
+/// (RotationConfig, MyRotation, AvatarAbility, ArenaSeason, BattlePass, DailyLoginBonus,
+/// PreReleaseInfo). Each test instantiates the importer in isolation and verifies it inserts
+/// rows from the corresponding seed file under Data/seeds/. Idempotency is spot-checked
+/// in one test (BattlePass) to avoid duplicating the canonical 4-test set per importer.
+///
+public class LoadIndexImporterTests
+{
+ private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
+
+ [Test]
+ public async Task RotationConfigImporter_writes_game_config_sections()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new RotationConfigImporter().ImportAsync(db, SeedDir);
+
+ var rows = await db.GameConfigs.ToListAsync();
+ Assert.That(rows.Any(r => r.SectionName == "Rotation"), Is.True,
+ "Rotation section must be written from rotation-config.json");
+ }
+
+ [Test]
+ public async Task MyRotationImporter_writes_settings_and_abilities()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new MyRotationImporter().ImportAsync(db, SeedDir);
+
+ Assert.That(await db.MyRotationSettings.CountAsync(), Is.GreaterThan(0),
+ "my-rotation-settings.json must produce setting rows");
+ Assert.That(await db.MyRotationAbilities.CountAsync(), Is.GreaterThan(0),
+ "my-rotation-abilities.json must produce ability rows");
+ }
+
+ [Test]
+ public async Task AvatarAbilityImporter_writes_abilities()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new AvatarAbilityImporter().ImportAsync(db, SeedDir);
+
+ Assert.That(await db.AvatarAbilities.CountAsync(), Is.GreaterThan(0),
+ "avatar-abilities.json must produce ability rows");
+ }
+
+ [Test]
+ public async Task ArenaSeasonImporter_writes_singleton()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new ArenaSeasonImporter().ImportAsync(db, SeedDir);
+
+ var row = await db.ArenaSeasons.FirstOrDefaultAsync(e => e.Id == 1);
+ Assert.That(row, Is.Not.Null, "ArenaSeason singleton id=1 must be written");
+ Assert.That(row!.FormatInfo, Is.Not.EqualTo("{}"), "format_info blob must be populated from seed");
+ }
+
+ [Test]
+ public async Task BattlePassImporter_writes_levels_and_is_idempotent()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new BattlePassImporter().ImportAsync(db, SeedDir);
+ int after1 = await db.BattlePassLevels.CountAsync();
+ await new BattlePassImporter().ImportAsync(db, SeedDir);
+ int after2 = await db.BattlePassLevels.CountAsync();
+
+ Assert.That(after1, Is.GreaterThan(0), "battle-pass-levels.json must produce rows");
+ Assert.That(after2, Is.EqualTo(after1), "rerun must be idempotent (no new rows)");
+ }
+
+ [Test]
+ public async Task DailyLoginBonusImporter_writes_bonus_rows()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new DailyLoginBonusImporter().ImportAsync(db, SeedDir);
+
+ Assert.That(await db.DailyLoginBonuses.CountAsync(), Is.GreaterThan(0),
+ "daily-login-bonus.json must produce rows");
+ }
+
+ [Test]
+ public async Task PreReleaseInfoImporter_writes_singleton()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new PreReleaseInfoImporter().ImportAsync(db, SeedDir);
+
+ var row = await db.PreReleaseInfos.FirstOrDefaultAsync(e => e.Id == 1);
+ Assert.That(row, Is.Not.Null, "PreReleaseInfo singleton id=1 must be written");
+ Assert.That(row!.PreReleaseId, Is.Not.Empty, "pre_release_id field must be populated");
+ }
+}