using System.Text.Json; using System.Text.Json.Serialization; using Chessistics.Engine.Model; namespace Chessistics.Engine.Loading; public static class CampaignLoader { private static readonly JsonSerializerOptions Options = new() { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; public static CampaignDef Load(string json) { var dto = JsonSerializer.Deserialize(json, Options) ?? throw new JsonException("Failed to deserialize campaign JSON."); Validate(dto); return new CampaignDef { Name = dto.Name, InitialWidth = dto.InitialWidth, InitialHeight = dto.InitialHeight, Missions = dto.Missions.Select(ParseMission).ToList() }; } public static CampaignDef LoadFromFile(string path) { var json = File.ReadAllText(path); return Load(json); } private static MissionDef ParseMission(MissionDto m) { var patch = new TerrainPatch { NewWidth = m.TerrainPatch.NewWidth, NewHeight = m.TerrainPatch.NewHeight, Cells = m.TerrainPatch.Cells.Select(ParsePatchCell).ToList() }; return new MissionDef { Id = m.Id, Name = m.Name, Description = m.Description ?? "", Flavor = m.Flavor ?? "", TerrainPatch = patch, Stock = m.Stock?.Select(s => new PieceStock(ParseKind(s.Kind), s.Count, s.Level)).ToList() ?? [], UnlockedPieces = m.UnlockedPieces?.Select(ParseKind).ToList() ?? [], UnlockedLevels = m.UnlockedLevels?.Select(u => new PieceUpgrade(ParseKind(u.Kind), u.Level)).ToList() ?? [] }; } private static PatchCell ParsePatchCell(CellDto c) { var cellType = c.Type.ToLowerInvariant() switch { "empty" => CellType.Empty, "wall" => CellType.Wall, "production" => CellType.Production, "demand" => CellType.Demand, "transformer" => CellType.Transformer, _ => throw new JsonException($"Unknown cell type: '{c.Type}'") }; ProductionDef? prod = null; if (cellType == CellType.Production && c.Production != null) { prod = new ProductionDef(new Coords(c.Col, c.Row), c.Production.Name, ParseCargo(c.Production.Cargo), c.Production.Amount); } DemandDef? demand = null; if (cellType == CellType.Demand && c.Demand != null) { demand = new DemandDef(new Coords(c.Col, c.Row), c.Demand.Name, ParseCargo(c.Demand.Cargo), c.Demand.Amount); } TransformerDef? transformer = null; if (cellType == CellType.Transformer && c.Transformer != null) { transformer = new TransformerDef( new Coords(c.Col, c.Row), c.Transformer.Name, ParseCargo(c.Transformer.InputCargo), c.Transformer.InputRequired, ParseCargo(c.Transformer.OutputCargo), c.Transformer.OutputAmount); } return new PatchCell { Col = c.Col, Row = c.Row, Type = cellType, Production = prod, Demand = demand, Transformer = transformer }; } private static CargoType ParseCargo(string cargo) => cargo.ToLowerInvariant() switch { "wood" => CargoType.Wood, "stone" => CargoType.Stone, "tools" => CargoType.Tools, "arms" => CargoType.Arms, "gold" => CargoType.Gold, _ => throw new JsonException($"Unknown cargo type: '{cargo}'") }; private static PieceKind ParseKind(string kind) => kind.ToLowerInvariant() switch { "pawn" => PieceKind.Pawn, "rook" => PieceKind.Rook, "bishop" => PieceKind.Bishop, "knight" => PieceKind.Knight, "queen" => PieceKind.Queen, _ => throw new JsonException($"Unknown piece kind: '{kind}'") }; private static void Validate(CampaignDto dto) { if (string.IsNullOrWhiteSpace(dto.Name)) throw new JsonException("Campaign name is required."); if (dto.InitialWidth <= 0 || dto.InitialHeight <= 0) throw new JsonException("Campaign dimensions must be positive."); if (dto.Missions.Count == 0) throw new JsonException("Campaign must have at least one mission."); int prevWidth = dto.InitialWidth; int prevHeight = dto.InitialHeight; foreach (var mission in dto.Missions) { if (mission.TerrainPatch.NewWidth < prevWidth || mission.TerrainPatch.NewHeight < prevHeight) throw new JsonException($"Mission '{mission.Name}': terrain cannot shrink (was {prevWidth}x{prevHeight}, got {mission.TerrainPatch.NewWidth}x{mission.TerrainPatch.NewHeight})."); prevWidth = mission.TerrainPatch.NewWidth; prevHeight = mission.TerrainPatch.NewHeight; } } // DTOs private class CampaignDto { public string Name { get; set; } = ""; public int InitialWidth { get; set; } public int InitialHeight { get; set; } public List Missions { get; set; } = []; } private class MissionDto { public int Id { get; set; } public string Name { get; set; } = ""; public string? Description { get; set; } public string? Flavor { get; set; } public TerrainPatchDto TerrainPatch { get; set; } = new(); public List? Stock { get; set; } public List? UnlockedPieces { get; set; } public List? UnlockedLevels { get; set; } } private class TerrainPatchDto { public int NewWidth { get; set; } public int NewHeight { get; set; } public List Cells { get; set; } = []; } private class CellDto { public int Col { get; set; } public int Row { get; set; } public string Type { get; set; } = "empty"; public ProductionDto? Production { get; set; } public DemandDto? Demand { get; set; } public TransformerDto? Transformer { get; set; } } private class ProductionDto { public string Name { get; set; } = ""; public string Cargo { get; set; } = ""; public int Amount { get; set; } = 1; } private class DemandDto { public string Name { get; set; } = ""; public string Cargo { get; set; } = ""; public int Amount { get; set; } } private class TransformerDto { public string Name { get; set; } = ""; public string InputCargo { get; set; } = ""; public int InputRequired { get; set; } = 1; public string OutputCargo { get; set; } = ""; public int OutputAmount { get; set; } = 1; } private class StockDto { public string Kind { get; set; } = ""; public int Count { get; set; } public int Level { get; set; } = 1; } private class UpgradeDto { public string Kind { get; set; } = ""; public int Level { get; set; } } }