using Theriapolis.Core.Data; namespace Theriapolis.Tools.Commands; /// /// Phase 6 M3 — dialogue-validate. Loads every dialogues/*.json /// through (which already /// validates structure + references) and prints a per-tree summary. /// CI gate: exits non-zero on any validation error. /// /// Usage: /// dotnet run --project Theriapolis.Tools -- dialogue-validate /// dotnet run --project Theriapolis.Tools -- dialogue-validate --data-dir ./Content/Data /// public static class DialogueValidate { 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 dialogues in: {dataDir}"); var loader = new ContentLoader(dataDir); DialogueDef[] defs; try { // Load items for cross-reference (give_item / has_item). var items = loader.LoadItems(); defs = loader.LoadDialogues(items); } catch (Exception ex) { Console.Error.WriteLine($" ✗ {ex.Message}"); return 1; } Console.WriteLine($" ✓ {defs.Length} dialogue tree(s) loaded."); foreach (var d in defs.OrderBy(d => d.Id, StringComparer.Ordinal)) { int totalOptions = d.Nodes.Sum(n => n.Options.Length); int skillChecks = d.Nodes.Sum(n => n.Options.Count(o => o.SkillCheck is not null)); Console.WriteLine($" [{d.Id}] root='{d.Root}' nodes={d.Nodes.Length} options={totalOptions} skill_checks={skillChecks}"); // Reachability: BFS from root over options.next + next_on_*. var reachable = new HashSet(StringComparer.OrdinalIgnoreCase) { d.Root }; var queue = new Queue(); queue.Enqueue(d.Root); var nodesById = d.Nodes.ToDictionary(n => n.Id, StringComparer.OrdinalIgnoreCase); while (queue.Count > 0) { var cur = queue.Dequeue(); if (!nodesById.TryGetValue(cur, out var node)) continue; foreach (var opt in node.Options) { foreach (var n in new[] { opt.Next, opt.NextOnSuccess, opt.NextOnFailure }) { if (string.IsNullOrEmpty(n) || string.Equals(n, "", StringComparison.OrdinalIgnoreCase)) continue; if (reachable.Add(n)) queue.Enqueue(n); } } } int unreachable = d.Nodes.Length - reachable.Count; if (unreachable > 0) { Console.Error.WriteLine($" ✗ {unreachable} unreachable node(s) in '{d.Id}': " + string.Join(", ", d.Nodes.Where(n => !reachable.Contains(n.Id)).Select(n => n.Id))); return 1; } } Console.WriteLine(); Console.WriteLine("All dialogue trees valid."); return 0; } }