156 lines
6.3 KiB
C#
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++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|