Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

156 lines
6.3 KiB
C#

using Theriapolis.Core.Data;
namespace Theriapolis.Tools.Commands;
/// <summary>
/// Loads every Phase 5 content file via <see cref="ContentLoader"/>, 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
/// </summary>
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<T>(string label, Func<T> 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++;
}
}
}