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); } }