using System.Collections.Generic; using Jovian.EncounterSystem; using UnityEditor; using UnityEngine; namespace Jovian.EncounterSystem.Editor { /// Severity of a . public enum ValidationSeverity { Warning, Error } /// /// One thing the validator found wrong. is the click-through target /// (table, dialog option set, reward), identifies the specific /// encounter the issue pertains to when applicable, and describes where /// inside the asset the issue lives. /// public class ValidationIssue { public Object asset; public Encounter encounter; public string path; public ValidationSeverity severity; public string message; } /// /// Scans every and asset in the project and /// returns the list of issues found. Intended to be driven from a menu item, the browser window, /// or pre-commit tooling. Runs on demand — does not cache. /// public static class EncounterValidator { /// Walk every encounter table and reward asset, returning all issues found. public static List ValidateProject() { var issues = new List(); var tables = FindAssetsOfType(); var idIndex = new Dictionary(); foreach(var table in tables) { ValidateTable(table, issues, idIndex); } foreach(var reward in FindAssetsOfType()) { ValidateReward(reward, issues); } return issues; } /// Return the issues for a single encounter inside a table. Useful for per-row badges. public static List ValidateEncounter(EncounterTable table, int index) { var issues = new List(); if(table?.encounters == null || index < 0 || index >= table.encounters.Count) { return issues; } var encounter = table.encounters[index]; ValidateEncounterEntry(table, index, encounter, issues, duplicateCheck: null); return issues; } private static void ValidateTable(EncounterTable table, List issues, Dictionary idIndex) { if(table?.encounters == null) { return; } for(int i = 0; i < table.encounters.Count; i++) { ValidateEncounterEntry(table, i, table.encounters[i], issues, idIndex); } } private static void ValidateEncounterEntry(EncounterTable table, int index, Encounter encounter, List issues, Dictionary duplicateCheck) { var pathPrefix = $"encounters[{index}]"; if(encounter == null) { issues.Add(new ValidationIssue { asset = table, path = pathPrefix, severity = ValidationSeverity.Error, message = "Null encounter entry." }); return; } var definition = encounter.EncounterDefinition; if(definition == null || string.IsNullOrEmpty(definition.internalId)) { issues.Add(new ValidationIssue { asset = table, encounter = encounter, path = $"{pathPrefix}.EncounterDefinition.internalId", severity = ValidationSeverity.Error, message = "Missing internalId (cannot be referenced by EncounterLink)." }); } else if(duplicateCheck != null) { if(duplicateCheck.TryGetValue(definition.internalId, out var prior)) { issues.Add(new ValidationIssue { asset = table, encounter = encounter, path = $"{pathPrefix}.EncounterDefinition.internalId", severity = ValidationSeverity.Error, message = $"Duplicate internalId '{definition.internalId}' (also in table '{prior.Item2.name}')." }); } else { duplicateCheck[definition.internalId] = (encounter, table); } } if(encounter.Kind == null) { issues.Add(new ValidationIssue { asset = table, encounter = encounter, path = $"{pathPrefix}.Kind", severity = ValidationSeverity.Warning, message = "Encounter.Kind is null — pick a kind in the inspector." }); } if(encounter.Kind is QuestKind questKind) { ValidateEncounterLink(table, pathPrefix + ".Kind.nextEncounter", encounter, questKind.nextEncounter, issues); } ValidateDialogEvents(table, index, encounter, issues); } private static void ValidateEncounterLink(EncounterTable owningTable, string path, Encounter encounter, EncounterLink link, List issues) { if(link.table == null && string.IsNullOrEmpty(link.internalId)) { return; } if(link.table == null) { issues.Add(new ValidationIssue { asset = owningTable, encounter = encounter, path = path, severity = ValidationSeverity.Error, message = "EncounterLink has internalId set but no table assigned." }); return; } if(link.Resolve() == null) { issues.Add(new ValidationIssue { asset = owningTable, encounter = encounter, path = path, severity = ValidationSeverity.Error, message = $"Broken EncounterLink — table '{link.table.name}' has no encounter with internalId '{link.internalId}'." }); } } private static void ValidateDialogEvents(EncounterTable table, int index, Encounter encounter, List issues) { var optionSet = encounter.EncounterDialogOptionSet; if(optionSet?.options == null) { return; } for(int o = 0; o < optionSet.options.Count; o++) { var option = optionSet.options[o]; if(option?.events == null) { continue; } for(int e = 0; e < option.events.Count; e++) { var evt = option.events[e]; if(evt == null) { issues.Add(new ValidationIssue { asset = optionSet, encounter = encounter, path = $"options[{o}].events[{e}]", severity = ValidationSeverity.Warning, message = "Null event entry (type may have been renamed/deleted)." }); continue; } if(evt is GiveRewardEvent give && give.reward == null) { issues.Add(new ValidationIssue { asset = optionSet, encounter = encounter, path = $"options[{o}].events[{e}].reward", severity = ValidationSeverity.Warning, message = "GiveRewardEvent has no Reward asset assigned." }); } } } } private static void ValidateReward(Reward reward, List issues) { if(reward == null) { return; } if(reward.kind == null) { issues.Add(new ValidationIssue { asset = reward, path = "kind", severity = ValidationSeverity.Warning, message = "Reward.kind is null — pick a reward kind in the inspector." }); } if(string.IsNullOrEmpty(reward.id)) { issues.Add(new ValidationIssue { asset = reward, path = "id", severity = ValidationSeverity.Warning, message = "Reward has no id." }); } } private static List FindAssetsOfType() where T : Object { var result = new List(); var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name); foreach(var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if(asset != null) { result.Add(asset); } } return result; } } /// Menu-driven runner that prints the validator report to the console with click-through asset pings. public static class EncounterValidatorMenu { [MenuItem("Jovian/Encounters/Validate All")] public static void ValidateAll() { var issues = EncounterValidator.ValidateProject(); if(issues.Count == 0) { Debug.Log("[EncounterValidator] No issues found."); return; } int errors = 0; int warnings = 0; foreach(var issue in issues) { if(issue.severity == ValidationSeverity.Error) { errors++; } else { warnings++; } } Debug.Log($"[EncounterValidator] {issues.Count} issue(s) found — {errors} error(s), {warnings} warning(s). Click any log row to ping the offending asset."); foreach(var issue in issues) { var assetName = issue.asset != null ? issue.asset.name : ""; var message = $"[EncounterValidator] {assetName} · {issue.path} — {issue.message}"; if(issue.severity == ValidationSeverity.Error) { Debug.LogError(message, issue.asset); } else { Debug.LogWarning(message, issue.asset); } } } } }