Files
encounter-system/Editor/EncounterValidator.cs
2026-04-19 12:46:26 +02:00

297 lines
12 KiB
C#

using System.Collections.Generic;
using Jovian.EncounterSystem;
using UnityEditor;
using UnityEngine;
namespace Jovian.EncounterSystem.Editor {
public enum ValidationSeverity {
Warning,
Error
}
public class ValidationIssue {
public Object asset;
public Encounter encounter;
public string path;
public ValidationSeverity severity;
public string message;
}
/// <summary>Project-wide scan of encounter tables and rewards. Runs on demand, no caching.</summary>
public static class EncounterValidator {
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;
}
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 == null) {
continue;
}
ValidateDialogLineRef(optionSet, encounter, $"options[{o}].text", option.text, issues);
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 ValidateDialogLineRef(EncounterDialogOptionSet optionSet, Encounter encounter, string path, DialogLineRef lineRef, List<ValidationIssue> issues) {
var library = optionSet.library;
var hasId = !string.IsNullOrEmpty(lineRef.id);
var hasInline = !string.IsNullOrEmpty(lineRef.inlineText);
if(!hasId && !hasInline) {
issues.Add(new ValidationIssue {
asset = optionSet,
encounter = encounter,
path = path,
severity = ValidationSeverity.Warning,
message = "Dialog line is empty (no id and no inline text)."
});
return;
}
if(hasId && library == null) {
issues.Add(new ValidationIssue {
asset = optionSet,
encounter = encounter,
path = path,
severity = ValidationSeverity.Error,
message = $"DialogLineRef references id '{lineRef.id}' but the option set has no library assigned."
});
return;
}
if(hasId && library != null && string.IsNullOrEmpty(library.Resolve(lineRef.id))) {
issues.Add(new ValidationIssue {
asset = optionSet,
encounter = encounter,
path = path,
severity = ValidationSeverity.Error,
message = $"DialogLineRef id '{lineRef.id}' not found in library '{library.name}'."
});
}
}
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;
}
}
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);
}
}
}
}
}