267 lines
11 KiB
C#
267 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using Jovian.EncounterSystem;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Jovian.EncounterSystem.Editor {
|
|
/// <summary>Severity of a <see cref="ValidationIssue"/>.</summary>
|
|
public enum ValidationSeverity {
|
|
Warning,
|
|
Error
|
|
}
|
|
|
|
/// <summary>
|
|
/// One thing the validator found wrong. <see cref="asset"/> is the click-through target
|
|
/// (table, dialog option set, reward), <see cref="encounter"/> identifies the specific
|
|
/// encounter the issue pertains to when applicable, and <see cref="path"/> describes where
|
|
/// inside the asset the issue lives.
|
|
/// </summary>
|
|
public class ValidationIssue {
|
|
public Object asset;
|
|
public Encounter encounter;
|
|
public string path;
|
|
public ValidationSeverity severity;
|
|
public string message;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans every <see cref="EncounterTable"/> and <see cref="Reward"/> 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.
|
|
/// </summary>
|
|
public static class EncounterValidator {
|
|
/// <summary>Walk every encounter table and reward asset, returning all issues found.</summary>
|
|
public static List<ValidationIssue> ValidateProject() {
|
|
var issues = new List<ValidationIssue>();
|
|
|
|
var tables = FindAssetsOfType<EncounterTable>();
|
|
var idIndex = new Dictionary<string, (Encounter encounter, EncounterTable table)>();
|
|
|
|
foreach(var table in tables) {
|
|
ValidateTable(table, issues, idIndex);
|
|
}
|
|
|
|
foreach(var reward in FindAssetsOfType<Reward>()) {
|
|
ValidateReward(reward, issues);
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/// <summary>Return the issues for a single encounter inside a table. Useful for per-row badges.</summary>
|
|
public static List<ValidationIssue> ValidateEncounter(EncounterTable table, int index) {
|
|
var issues = new List<ValidationIssue>();
|
|
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<ValidationIssue> issues, Dictionary<string, (Encounter, EncounterTable)> 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<ValidationIssue> issues, Dictionary<string, (Encounter, EncounterTable)> 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<ValidationIssue> 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<ValidationIssue> 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<ValidationIssue> 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<T> FindAssetsOfType<T>() where T : Object {
|
|
var result = new List<T>();
|
|
var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name);
|
|
foreach(var guid in guids) {
|
|
var path = AssetDatabase.GUIDToAssetPath(guid);
|
|
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
|
if(asset != null) {
|
|
result.Add(asset);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>Menu-driven runner that prints the validator report to the console with click-through asset pings.</summary>
|
|
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 : "<null>";
|
|
var message = $"[EncounterValidator] {assetName} · {issue.path} — {issue.message}";
|
|
if(issue.severity == ValidationSeverity.Error) {
|
|
Debug.LogError(message, issue.asset);
|
|
}
|
|
else {
|
|
Debug.LogWarning(message, issue.asset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|