using Theriapolis.Core.Data; namespace Theriapolis.Tools.Commands; /// /// Loads every Phase 5 content file via , runs /// per-file referential integrity checks, and exits non-zero on any error. /// CI runs this so a broken JSON edit fails the build instead of crashing /// the game at runtime. /// /// Usage: /// dotnet run --project Theriapolis.Tools -- content-validate /// dotnet run --project Theriapolis.Tools -- content-validate --data-dir ./Content/Data /// public static class ContentValidate { public static int Run(string[] args) { string dataDir = "./Content/Data"; for (int i = 0; i < args.Length; i++) { if (args[i] == "--data-dir" && i + 1 < args.Length) { dataDir = args[i + 1]; i++; } } Console.WriteLine($"Validating content in: {dataDir}"); var loader = new ContentLoader(dataDir); int errorCount = 0; // Phase 1+ content (already validated elsewhere, but check it loads) var biomes = TryLoad("biomes", () => loader.LoadBiomes(), ref errorCount); var factions = TryLoad("factions", () => loader.LoadFactions(), ref errorCount); TryLoad("macro_template", () => loader.LoadMacroTemplate(), ref errorCount); // Phase 5 content var clades = TryLoad("clades", () => loader.LoadClades(), ref errorCount); var species = clades is not null ? TryLoad("species", () => loader.LoadSpecies(clades), ref errorCount) : null; var classes = TryLoad("classes", () => loader.LoadClasses(), ref errorCount); if (classes is not null) TryLoad("subclasses", () => loader.LoadSubclasses(classes), ref errorCount); TryLoad("backgrounds", () => loader.LoadBackgrounds(), ref errorCount); var items = TryLoad("items", () => loader.LoadItems(), ref errorCount); TryLoad("npc_templates", () => loader.LoadNpcTemplates(items, factions), ref errorCount); TryLoad("loot_tables", () => loader.LoadLootTables(items), ref errorCount); // Phase 6 M0 content var buildings = TryLoad("building_templates", () => loader.LoadBuildingTemplates(), ref errorCount); var layouts = buildings is not null ? TryLoad("settlement_layouts", () => loader.LoadSettlementLayouts(buildings), ref errorCount) : null; // Phase 6 M1 content var biasProfiles = TryLoad("bias_profiles", () => loader.LoadBiasProfiles(clades, factions), ref errorCount); var residents = TryLoad("resident_templates", () => loader.LoadResidentTemplates(biasProfiles, clades, species, factions), ref errorCount); // Phase 6 M3 content var dialogues = TryLoad("dialogues", () => loader.LoadDialogues(items), ref errorCount); // Phase 6 M4 content var quests = TryLoad("quests", () => loader.LoadQuests(items), ref errorCount); // Phase 7 M0 content var lootTables = items is not null ? TryLoad("loot_tables_for_dungeons", () => loader.LoadLootTables(items), ref errorCount) : null; var roomTemplates = TryLoad("room_templates", () => loader.LoadRoomTemplates(), ref errorCount); var dungeonLayouts = TryLoad("dungeon_layouts", () => loader.LoadDungeonLayouts(roomTemplates, lootTables), ref errorCount); // Cross-file referential checks if (clades is not null && species is not null) { CrossCheck( "every species references a real clade", () => { var cladeIds = clades.Select(c => c.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var sp in species) if (!cladeIds.Contains(sp.CladeId)) throw new InvalidDataException($"species '{sp.Id}' references unknown clade_id '{sp.CladeId}'"); }, ref errorCount); } if (errorCount == 0) { Console.WriteLine(); Console.WriteLine("All content valid."); Console.WriteLine($" {biomes?.Length ?? 0} biomes"); Console.WriteLine($" {factions?.Length ?? 0} factions"); Console.WriteLine($" {clades?.Length ?? 0} clades"); Console.WriteLine($" {species?.Length ?? 0} species"); Console.WriteLine($" {classes?.Length ?? 0} classes"); Console.WriteLine($" {items?.Length ?? 0} items"); Console.WriteLine($" {buildings?.Length ?? 0} building templates"); Console.WriteLine($" {layouts?.Length ?? 0} settlement layouts"); Console.WriteLine($" {biasProfiles?.Length ?? 0} bias profiles"); Console.WriteLine($" {residents?.Length ?? 0} resident templates"); Console.WriteLine($" {dialogues?.Length ?? 0} dialogue trees"); Console.WriteLine($" {quests?.Length ?? 0} quest trees"); Console.WriteLine($" {roomTemplates?.Length ?? 0} room templates"); Console.WriteLine($" {dungeonLayouts?.Length ?? 0} dungeon layouts"); return 0; } Console.Error.WriteLine(); Console.Error.WriteLine($"Validation failed with {errorCount} error(s)."); return 1; } private static T? TryLoad(string label, Func action, ref int errorCount) where T : class { try { var result = action(); Console.WriteLine($" ✓ {label}"); return result; } catch (Exception ex) { Console.Error.WriteLine($" ✗ {label}: {ex.Message}"); errorCount++; return null; } } private static void CrossCheck(string description, Action check, ref int errorCount) { try { check(); Console.WriteLine($" ✓ {description}"); } catch (Exception ex) { Console.Error.WriteLine($" ✗ {description}: {ex.Message}"); errorCount++; } } }