Added the system from trail

This commit is contained in:
Sebastian Bularca
2026-04-19 12:25:49 +02:00
parent 47ee77f272
commit 89e36b4df9
57 changed files with 2160 additions and 15 deletions

View File

@@ -0,0 +1,266 @@
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);
}
}
}
}
}