├── .gitignore ├── Documentation~ ├── Documentation.md ├── HeaderImage.jpg ├── TestRunner.jpg ├── ValidationOverlay.png ├── ValidationWindow.png └── ValidationWindowMeme.jpg ├── Editor.meta ├── Editor ├── AssetDependencyPathValidator.cs ├── AssetDependencyPathValidator.cs.meta ├── ComponentIdentifier.cs ├── ComponentIdentifier.cs.meta ├── CoroutineUtilities.cs ├── CoroutineUtilities.cs.meta ├── Data.meta ├── Data │ ├── Issue.cs │ ├── Issue.cs.meta │ ├── IssueCollection.cs │ ├── IssueCollection.cs.meta │ ├── SourceInfo.cs │ ├── SourceInfo.cs.meta │ ├── ValidationScope.cs │ └── ValidationScope.cs.meta ├── DefaultProvider.cs ├── DefaultProvider.cs.meta ├── ExtensionsMethods.cs ├── ExtensionsMethods.cs.meta ├── PrefabUsageValidation.cs ├── PrefabUsageValidation.cs.meta ├── SceneValidator.cs ├── SceneValidator.cs.meta ├── SerializedFieldValidationHandler.cs ├── SerializedFieldValidationHandler.cs.meta ├── SerializedFieldValidator.cs ├── SerializedFieldValidator.cs.meta ├── SourceCodeParser.cs ├── SourceCodeParser.cs.meta ├── Triband.Validation.Editor.asmdef ├── Triband.Validation.Editor.asmdef.meta ├── UI.meta ├── UI │ ├── ValidationOverlay.cs │ ├── ValidationOverlay.cs.meta │ ├── ValidationResult.uxml │ ├── ValidationResult.uxml.meta │ ├── ValidationWindow.cs │ ├── ValidationWindow.cs.meta │ ├── ValidationWindow.uxml │ ├── ValidationWindow.uxml.meta │ ├── com.triband.validation.uss │ └── com.triband.validation.uss.meta ├── ValidationContext.cs ├── ValidationContext.cs.meta ├── ValidationCore.cs ├── ValidationCore.cs.meta ├── ValidationManager.cs ├── ValidationManager.cs.meta ├── Validator.cs └── Validator.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── Assembly.cs ├── Assembly.cs.meta ├── Attributes.meta ├── Attributes │ ├── OptionalReferenceAttribute.cs │ ├── OptionalReferenceAttribute.cs.meta │ ├── OptionalReferenceInPrefabAttribute.cs │ └── OptionalReferenceInPrefabAttribute.cs.meta ├── IValidationContext.cs ├── IValidationContext.cs.meta ├── Interface.meta ├── Interface │ ├── IRequireValidation.cs │ ├── IRequireValidation.cs.meta │ ├── IValidationParent.cs │ ├── IValidationParent.cs.meta │ ├── IValidationSystemSceneProvider.cs │ └── IValidationSystemSceneProvider.cs.meta ├── Triband.Validation.Runtime.asmdef ├── Triband.Validation.Runtime.asmdef.meta ├── ValidationSeverity.cs ├── ValidationSeverity.cs.meta ├── ValidationUtils.cs └── ValidationUtils.cs.meta ├── Samples~ ├── AssetDependencyValidation.meta ├── AssetDependencyValidation │ ├── AssetDependencyValidator.cs │ └── AssetDependencyValidator.cs.meta ├── NullReferenceCheck.meta └── NullReferenceCheck │ ├── NullRefValidator.cs │ ├── NullRefValidator.cs.meta │ ├── RequiredIfAttribute.cs │ └── RequiredIfAttribute.cs.meta ├── Tests.meta ├── Tests ├── Editor.meta └── Editor │ ├── SceneTesting.cs │ ├── SceneTesting.cs.meta │ ├── Triband.Validation.Runtime.asmdef │ ├── Triband.Validation.Runtime.asmdef.meta │ ├── ValidationTestProviderUtility.cs │ └── ValidationTestProviderUtility.cs.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | /[Bb]uilds/ 6 | /[Ll]ogs/ 7 | /[Uu]ser[Ss]ettings/ 8 | 9 | # MemoryCaptures can get excessive in size. 10 | # They also could contain extremely sensitive data 11 | /[Mm]emoryCaptures/ 12 | 13 | # Recordings can get excessive in size 14 | /[Rr]ecordings/ 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | /[Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | /.vscode/ 23 | .vs/ 24 | 25 | # JetBrains Intellij/Rider 26 | .idea 27 | # Gradle cache directory 28 | .gradle 29 | 30 | # Autogenerated VS/MD/Consulo solution and project files 31 | ExportedObj/ 32 | .consulo/ 33 | *.csproj 34 | *.unityproj 35 | *.sln 36 | *.suo 37 | *.tmp 38 | *.user 39 | *.userprefs 40 | *.pidb 41 | *.booproj 42 | *.svd 43 | *.pdb 44 | *.mdb 45 | *.opendb 46 | *.VC.db 47 | 48 | # npm packages related 49 | /cache/ 50 | 51 | # Unity3D generated meta files 52 | *.pidb.meta 53 | *.pdb.meta 54 | *.mdb.meta 55 | 56 | # Unity3D generated file on crash reports 57 | sysinfo.txt 58 | 59 | # Builds 60 | *.apk 61 | *.aab 62 | *.unitypackage 63 | *.app 64 | 65 | # Crashlytics generated file 66 | crashlytics-build.properties 67 | 68 | # Packed Addressables 69 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 70 | 71 | # Temporary auto-generated Android Assets 72 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 73 | /[Aa]ssets/[Ss]treamingAssets/aa/* 74 | -------------------------------------------------------------------------------- /Documentation~/Documentation.md: -------------------------------------------------------------------------------- 1 | ## Writing Validation Checks 2 | 3 | There are two ways to add validation checks to your project: 4 | 5 | You can a Validator class that inherits from *Validator*. For example, this Validator will report missing materials on Renderers: 6 | ```C# 7 | public class RendererValidator : Validator 8 | { 9 | //This method will be called for any Renderer within the currently open scene or prefab 10 | protected override void Validate(Renderer renderer, IValidationContext context) 11 | { 12 | //Report if any Renderer does not have a material 13 | context.IsNotNull(renderer.sharedMaterial); 14 | } 15 | } 16 | ``` 17 | 18 | You can also place your validation logic directly inside of a MonoBehaviour by implementing *IRequireValidation*. This is very handy when you just want to add a quick little check to an existing class. 19 | ```C# 20 | public class DontMakeMeStatic : MonoBehaviour, IRequireValidation { 21 | /*all your regular code goes here*/ 22 | 23 | //overwrite Validate() to add validation checks to this behaviour 24 | public void Validate(IValidationContext context) { 25 | context.IsFalse(gameObject.isStatic); 26 | } 27 | } 28 | ``` 29 | 30 | The following validation methods are available in IValidationContext: 31 | 32 | * AreEqual(expected, actual) 33 | * AreNotEqual(expected, actual) 34 | * IsNotNull(object) 35 | * IsNull(object) 36 | * IsTrue(bool) 37 | * IsFalse(bool) 38 | 39 | Each of these messages returns the result of the check. This can be useful for nested checks, like in this example: 40 | 41 | ```C# 42 | //Ensure that the renderer has a material 43 | if (context.IsNotNull(renderer.sharedMaterial != null)) 44 | { 45 | //if so, check that the material has a texture 46 | context.IsNotNull(renderer.sharedMaterial.mainTexture); 47 | } 48 | ``` 49 | By placing the second check inside of the if statement, you prevent a NullReferenceException to be thrown if the material is null. 50 | 51 | ## SceneValidator 52 | SceneValidato classes allow you to run a check once per scene, instead of once per object of a type. Let's say, we have a mobile game where we want to restrict ourselves to use exactly one light per scene for performance reasons. We could add the following check to the project: 53 | 54 | ```C# 55 | public class OneLightValidator:SceneValidator{ 56 | public override void DoValidation(IValidationContext context) 57 | { 58 | context.AreEqual(1, Object.FindObjectsOfType().Length); 59 | } 60 | } 61 | ``` 62 | 63 | ## Error message generation 64 | The Validation system generates error messages based on the source code. In a lot of cases, these messages can be good enough out of the box. For example, this code 65 | ```C# 66 | context.AreEqual(LightType.Directional, light.type); 67 | //This code will generate the following error message: 68 | //"Light.type was expected to be Directional, but is Point" 69 | ``` 70 | 71 | You can improve the readability of your error messages by choosing a good name for the variables that go into the validation. 72 | ```C# 73 | bool lightPointsDown = Vector3.Angle(Vector3.down, light.transform.forward) < 90; 74 | context.IsTrue(lightPointsDown); 75 | //This code will generate the following error message: 76 | //"lightPointsDown was expected to be true, but is false" 77 | ``` 78 | 79 | However, you can also provide a custom callback for error message generation: 80 | ```C# 81 | context.AreEqual(LightType.Directional, light.type, errorText:()=>"Only Directional lights, please!"); 82 | ``` 83 | 84 | ## Severity 85 | It is possible to overwrite the severity of a validation issue. By default, everything is treated like an error, but you can overwrite it to be a warning instead. 86 | ```C# 87 | //report a warning if game object is not called Golfer 88 | context.AreEqual("Golfer", gameObject.name, severity: ValidationSeverity.Warning); 89 | ``` 90 | 91 | ## Auto Fixes 92 | Sometimes it is possible to provide an issue to be fixed by code. In this case, you can overwrite the optional parameter *autoFix*. A button labeled "Fix" will apperar in the Validation Results window. 93 | ```C# 94 | //report if the game object is not called Golfer. 95 | context.AreEqual("Golfer", gameObject.name, autoFix: ()=>{gameObject.name = "Golfer";}); 96 | ``` 97 | 98 | ## The Validation Overlay 99 | When adding the package to your project, this overlay is available in your scene view. It refreshes automatically when you open or save a scene or prefab. You can also click "Update" to re-run validation manually. 100 | Clicking "Open Validation Window" opens the Validation Window, which gives you a detailed report of the errors. 101 | 102 | ![image](ValidationOverlay.png) 103 | 104 | ## The Validation Window 105 | The Validation Window gives you an overview over all the issues in the open scene or prefab. 106 | Please note that the issues are grouped by the Object or Asset they appear in. 107 | 108 | The first issue in the screenshot happens directly inside of SampleScene.unity. 109 | 110 | However, the second error happens inside of a prefab called "GameObject.prefab". The identical check also fails for 7 instances of said prefab inside of the scene. You can open the foldout group to see the individual instances of the prefab that fail this check. However, you most likely want to fix this check inside of the prefab. 111 | When the issue is fixed inside of the prefab, it will also fix the issue in the scene. 112 | ![image](ValidationWindow.png) 113 | -------------------------------------------------------------------------------- /Documentation~/HeaderImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TribandApS/validation/d6f611e5db052ca39bcd35e4177363b7ce8fe708/Documentation~/HeaderImage.jpg -------------------------------------------------------------------------------- /Documentation~/TestRunner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TribandApS/validation/d6f611e5db052ca39bcd35e4177363b7ce8fe708/Documentation~/TestRunner.jpg -------------------------------------------------------------------------------- /Documentation~/ValidationOverlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TribandApS/validation/d6f611e5db052ca39bcd35e4177363b7ce8fe708/Documentation~/ValidationOverlay.png -------------------------------------------------------------------------------- /Documentation~/ValidationWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TribandApS/validation/d6f611e5db052ca39bcd35e4177363b7ce8fe708/Documentation~/ValidationWindow.png -------------------------------------------------------------------------------- /Documentation~/ValidationWindowMeme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TribandApS/validation/d6f611e5db052ca39bcd35e4177363b7ce8fe708/Documentation~/ValidationWindowMeme.jpg -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4fd9c9d74eeba3a40ad1c1b783efcae3 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/AssetDependencyPathValidator.cs: -------------------------------------------------------------------------------- 1 | using Triband.Validation.Runtime; 2 | 3 | namespace Triband.Validation.Editor 4 | { 5 | public abstract class AssetDependencyPathValidator 6 | { 7 | /// 8 | /// Evaluates whether a parent is allowed to have an dependency to a child. For example, 9 | /// parentPath might point to a scene and childPath to a texture used in the level. 10 | /// Return false if the level is not allowed to use the texture 11 | /// 12 | /// a prefab or scene that is used using the child 13 | /// a prefab or asset that is used by the parent 14 | /// 15 | 16 | public abstract void ValidatePath(string parentPath, string childPath, IValidationContext context); 17 | } 18 | } -------------------------------------------------------------------------------- /Editor/AssetDependencyPathValidator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ff2b242f10740ba8d0b258e3c747ade 3 | timeCreated: 1676300395 -------------------------------------------------------------------------------- /Editor/ComponentIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace Triband.Validation.Editor 8 | { 9 | public struct ComponentIdentifier 10 | { 11 | private sealed class GuidFileIDInstanceIDEqualityComparer : IEqualityComparer 12 | { 13 | public bool Equals(ComponentIdentifier x, ComponentIdentifier y) 14 | { 15 | return x.guid.Equals(y.guid) && x.fileID == y.fileID && x.instanceID == y.instanceID; 16 | } 17 | 18 | public int GetHashCode(ComponentIdentifier obj) 19 | { 20 | return HashCode.Combine(obj.guid, obj.fileID, obj.instanceID); 21 | } 22 | } 23 | 24 | public static IEqualityComparer GuidFileIDInstanceIDComparer { get; } = new GuidFileIDInstanceIDEqualityComparer(); 25 | 26 | public bool Equals(ComponentIdentifier other) 27 | { 28 | return guid.Equals(other.guid) && fileID == other.fileID && instanceID == other.instanceID; 29 | } 30 | 31 | public override bool Equals(object obj) 32 | { 33 | return obj is ComponentIdentifier other && Equals(other); 34 | } 35 | 36 | public override int GetHashCode() 37 | { 38 | return HashCode.Combine(guid, fileID, instanceID); 39 | } 40 | 41 | public ComponentIdentifier(Object obj) 42 | { 43 | if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out guid, out fileID)) 44 | { 45 | instanceID = -1; 46 | } 47 | else 48 | { 49 | instanceID = obj.GetInstanceID(); 50 | guid = string.Empty; 51 | fileID = -1; 52 | } 53 | 54 | } 55 | 56 | public override string ToString() 57 | { 58 | return $"{nameof(instanceID)}:{instanceID} {nameof(fileID)}:{fileID} {nameof(guid)}:{guid}"; 59 | } 60 | 61 | public readonly string guid; 62 | public readonly long fileID; 63 | public readonly int instanceID; 64 | 65 | //[MenuItem("Test/TestID")] 66 | public static void TryGet() 67 | { 68 | // Debug.Log(new ComponentIdentifier(Selection.activeGameObject.transform)); 69 | // 70 | // var obj = new SerializedObject(Selection.activeGameObject); 71 | // var serializedProperty = obj.FindProperty("m_LocalIdentifierInFile"); 72 | // Debug.Log(serializedProperty.longValue); 73 | 74 | var l = Selection.activeGameObject.GetComponent(); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Editor/ComponentIdentifier.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a81c75168d4f4a61908e3fd8c49e83ea 3 | timeCreated: 1676554716 -------------------------------------------------------------------------------- /Editor/CoroutineUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Unity.EditorCoroutines.Editor; 5 | 6 | namespace Triband.Validation.Editor 7 | { 8 | public static class CoroutineUtilities 9 | { 10 | public static void RunCoroutineSynchronously(IEnumerator coroutine) 11 | { 12 | var callstack = new Stack(); 13 | callstack.Push(coroutine); 14 | while (callstack.Count > 0) 15 | { 16 | var currentEnumerator = callstack.Peek(); 17 | if (currentEnumerator.MoveNext()) 18 | { 19 | if (currentEnumerator.Current != null) 20 | { 21 | if (currentEnumerator.Current is IEnumerator childEnumerator) 22 | { 23 | callstack.Push(childEnumerator); 24 | } 25 | } 26 | } 27 | else 28 | { 29 | callstack.Pop(); 30 | } 31 | } 32 | } 33 | 34 | public static IEnumerator RunCoroutineWithFixedInterval(IEnumerator coroutine, 35 | int intervalInMilliseconds = 10) 36 | { 37 | var stopwatch = new Stopwatch(); 38 | stopwatch.Start(); 39 | yield return null; 40 | 41 | var callstack = new Stack(); 42 | callstack.Push(coroutine); 43 | while (callstack.Count > 0) 44 | { 45 | var currentEnumerator = callstack.Peek(); 46 | if (currentEnumerator.MoveNext()) 47 | { 48 | if (currentEnumerator.Current != null) 49 | { 50 | if (currentEnumerator.Current is IEnumerator childEnumerator) 51 | { 52 | callstack.Push(childEnumerator); 53 | } 54 | } 55 | } 56 | else 57 | { 58 | callstack.Pop(); 59 | } 60 | 61 | if (stopwatch.ElapsedMilliseconds > intervalInMilliseconds) 62 | { 63 | stopwatch.Restart(); 64 | 65 | yield return new EditorWaitForSeconds(0); 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Editor/CoroutineUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a8d69d02631040639670814acd57a3a9 3 | timeCreated: 1676897863 -------------------------------------------------------------------------------- /Editor/Data.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ed25716d7fc13e645a67d9f61a06910b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Data/Issue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Triband.Validation.Runtime; 5 | using UnityEditor; 6 | using UnityEditor.SceneManagement; 7 | using Debug = System.Diagnostics.Debug; 8 | 9 | namespace Triband.Validation.Editor.Data 10 | { 11 | public class Issue 12 | { 13 | public readonly ValidationScope validationScope; 14 | public readonly SourceInfo sourceInfo; 15 | public readonly ValidationSeverity severity; 16 | 17 | public Issue parent { get; private set; } 18 | public string infoText { get; private set; } 19 | public Action autoFix { get; private set; } 20 | 21 | public List children { get; private set; } = null; 22 | 23 | public bool isDependency => validationScope.IsDependency; 24 | public bool isAsset => validationScope.Type == ValidationScope.ObjectType.Asset; 25 | 26 | internal Issue(ValidationScope validationScope, SourceInfo sourceInfo, string infoText, Action autoFix, ValidationSeverity severity) 27 | { 28 | this.validationScope = validationScope; 29 | this.sourceInfo = sourceInfo; 30 | this.infoText = infoText; 31 | this.autoFix = autoFix; 32 | this.severity = severity; 33 | } 34 | 35 | #if UNITY_EDITOR 36 | public void ApplyAutoFixIfPossible() 37 | { 38 | if (autoFix != null) 39 | { 40 | autoFix.Invoke(); 41 | 42 | if (validationScope.Type == ValidationScope.ObjectType.Prefab) 43 | { 44 | if (validationScope.IsDependency) 45 | { 46 | throw new Exception("Auto fixes to prefabs are not allowed to be applied to prefabs that are dependencies, since those " + 47 | "prefabs are not actually opened for editing and changing them from code will corrupt the prefab."); 48 | } 49 | } 50 | 51 | Debug.Assert(validationScope.Target != null, nameof(validationScope.Target) + " != null"); 52 | 53 | if (validationScope.Target != null) 54 | { 55 | EditorUtility.SetDirty(validationScope.Target); 56 | AssetDatabase.SaveAssetIfDirty(validationScope.Target); 57 | } 58 | 59 | if (validationScope.Type == ValidationScope.ObjectType.Scene) 60 | { 61 | EditorSceneManager.MarkAllScenesDirty(); 62 | } 63 | 64 | infoText = "fixed"; 65 | autoFix = null; 66 | } 67 | } 68 | #endif 69 | public void ResolveParent(List checks) 70 | { 71 | var validatedObject = validationScope.Target; 72 | if (validatedObject == null) 73 | { 74 | return; 75 | } 76 | var prefabObject = PrefabUtility.GetCorrespondingObjectFromSource(validatedObject); 77 | if (prefabObject == null) 78 | { 79 | return; 80 | } 81 | 82 | parent = checks.FirstOrDefault(otherCheck => 83 | { 84 | if (otherCheck.validationScope.Target != prefabObject) 85 | { 86 | return false; 87 | } 88 | 89 | return otherCheck.sourceInfo.Equals(sourceInfo); 90 | }); 91 | if (parent != null) 92 | { 93 | parent.RegisterChild(this); 94 | } 95 | } 96 | 97 | private void RegisterChild(Issue child) 98 | { 99 | if (children == null) 100 | { 101 | children = new List(); 102 | } 103 | children.Add(child); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Editor/Data/Issue.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f91a8405b49af2343aa7c842099aca70 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Data/IssueCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Triband.Validation.Editor.Data 4 | { 5 | public struct IssueCollection 6 | { 7 | public readonly List issues; 8 | 9 | public IssueCollection(List checks, string inspectedStage) 10 | { 11 | this.issues = new List(checks); 12 | 13 | foreach (var check in checks) 14 | { 15 | check.ResolveParent(checks); 16 | } 17 | 18 | inspectedStageInfoText = inspectedStage; 19 | } 20 | 21 | public static IssueCollection empty => new IssueCollection(null, ""); 22 | 23 | public readonly string inspectedStageInfoText; 24 | } 25 | } -------------------------------------------------------------------------------- /Editor/Data/IssueCollection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f73361a8699edb841b3df521a1e90251 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Data/SourceInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Triband.Validation.Editor.Data 4 | { 5 | public readonly struct SourceInfo 6 | { 7 | public bool Equals(SourceInfo other) 8 | { 9 | return filePath == other.filePath && sourceLine == other.sourceLine; 10 | } 11 | 12 | public override bool Equals(object obj) 13 | { 14 | return obj is SourceInfo other && Equals(other); 15 | } 16 | 17 | public override int GetHashCode() 18 | { 19 | return HashCode.Combine(filePath, sourceLine); 20 | } 21 | 22 | public readonly string filePath; 23 | public readonly int sourceLine; 24 | 25 | public SourceInfo(string filePath, int sourceLine) 26 | { 27 | this.filePath = filePath; 28 | this.sourceLine = sourceLine; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Editor/Data/SourceInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f01a35d50c924035810519c4bb236202 3 | timeCreated: 1676638919 -------------------------------------------------------------------------------- /Editor/Data/ValidationScope.cs: -------------------------------------------------------------------------------- 1 | using Triband.Validation.Runtime; 2 | using UnityEngine; 3 | 4 | namespace Triband.Validation.Editor.Data 5 | { 6 | public struct ValidationScope 7 | { 8 | public readonly Object Target; 9 | public ObjectType Type; 10 | 11 | /// 12 | /// If true, the scope is not the primary target of the current validation. For example, it might be a prefab 13 | /// that is used in a scene or an asset 14 | /// 15 | public bool IsDependency; 16 | 17 | public enum ObjectType 18 | { 19 | Scene, 20 | SceneObject, 21 | Prefab, 22 | Asset, 23 | } 24 | 25 | public ValidationScope(Object target, ObjectType type, bool isDependency) 26 | { 27 | Target = target; 28 | Type = type; 29 | IsDependency = isDependency; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Editor/Data/ValidationScope.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e614946d53ef428bb3bcfb84008b4790 3 | timeCreated: 1676638923 -------------------------------------------------------------------------------- /Editor/DefaultProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq; 3 | using Triband.Validation.Runtime.Interface; 4 | 5 | namespace Triband.Validation.Editor 6 | { 7 | public sealed class DefaultProvider : IValidationSystemSceneProvider 8 | { 9 | const string EXCLUDE_LABEL = "ExcludeValidation"; 10 | 11 | public string[] GetTestScenePaths() 12 | { 13 | return UnityEditor.EditorBuildSettings.scenes 14 | .Where(s => s.enabled) 15 | .Select(s => s.path) 16 | .Where(s => 17 | { 18 | var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(s); 19 | var labels = UnityEditor.AssetDatabase.GetLabels(asset); 20 | return !labels.Contains(EXCLUDE_LABEL); 21 | }).ToArray(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Editor/DefaultProvider.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 352dc9550122d4740b9011706d776f95 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ExtensionsMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Triband.Validation.Editor 7 | { 8 | public static class ExtensionsMethods 9 | { 10 | public static bool TryGetAttribute(this TArrayType[] array, out TDesiredType value) where TArrayType : Attribute where TDesiredType : Attribute 11 | { 12 | for (int i = 0; i < array.Length; i++) 13 | { 14 | if (array[i] is TDesiredType result) 15 | { 16 | value = result; 17 | return true; 18 | } 19 | } 20 | 21 | value = default; 22 | return false; 23 | } 24 | 25 | public static bool ContainsType(this Attribute[] array) where TDesiredType : Attribute 26 | { 27 | for (int i = 0; i < array.Length; i++) 28 | { 29 | if (array[i] is TDesiredType) 30 | { 31 | return true; 32 | } 33 | } 34 | 35 | return false; 36 | } 37 | } 38 | 39 | internal static class SerializedPropertyExtensions 40 | { 41 | //From https://forum.unity.com/threads/serialiedproperty-check-if-it-has-a-propertyattribute.436103/#post-7349540 42 | public static T GetAttribute(this SerializedProperty prop, bool inherit) where T : Attribute 43 | { 44 | if (prop == null) 45 | { 46 | return null; 47 | } 48 | 49 | Type t = prop.serializedObject.targetObject.GetType(); 50 | 51 | FieldInfo f = null; 52 | PropertyInfo p = null; 53 | foreach (string name in prop.propertyPath.Split('.')) 54 | { 55 | f = TryGetFieldInfoRecursive(t, name); 56 | 57 | if (f == null) 58 | { 59 | p = t.GetProperty(name, (BindingFlags)(-1)); 60 | if (p == null) 61 | { 62 | return null; 63 | } 64 | 65 | t = p.PropertyType; 66 | } 67 | else 68 | { 69 | t = f.FieldType; 70 | } 71 | } 72 | 73 | T[] attributes; 74 | 75 | if (f != null) 76 | { 77 | attributes = f.GetCustomAttributes(typeof(T), inherit) as T[]; 78 | } 79 | else if (p != null) 80 | { 81 | attributes = p.GetCustomAttributes(typeof(T), inherit) as T[]; 82 | } 83 | else 84 | { 85 | return null; 86 | } 87 | 88 | return attributes != null && attributes.Length > 0 ? attributes[0] : null; 89 | } 90 | 91 | public static Attribute[] GetAttributes(this SerializedProperty prop, bool inherit) 92 | { 93 | if (prop == null) 94 | { 95 | return null; 96 | } 97 | 98 | Type t = prop.serializedObject.targetObject.GetType(); 99 | 100 | FieldInfo f = null; 101 | PropertyInfo p = null; 102 | foreach (string name in prop.propertyPath.Split('.')) 103 | { 104 | f = TryGetFieldInfoRecursive(t, name); 105 | 106 | if (f == null) 107 | { 108 | p = t.GetProperty(name, (BindingFlags)(-1)); 109 | if (p == null) 110 | { 111 | return Array.Empty(); 112 | } 113 | 114 | t = p.PropertyType; 115 | } 116 | else 117 | { 118 | t = f.FieldType; 119 | } 120 | } 121 | 122 | if (f != null) 123 | { 124 | return f.GetCustomAttributes(inherit) as Attribute[]; 125 | } 126 | else if (p != null) 127 | { 128 | return p.GetCustomAttributes(inherit) as Attribute[]; 129 | } 130 | 131 | throw new ArgumentException( 132 | $"Failed to get Attributes for property {prop.propertyPath} in type {prop.serializedObject.targetObject.GetType().FullName}"); 133 | } 134 | 135 | public static FieldInfo TryGetFieldInfoRecursive(Type t, string name) 136 | { 137 | try 138 | { 139 | //all bind flags except IgnoreCase 140 | var bindingFlags = (BindingFlags)(-1) ^ BindingFlags.IgnoreCase; 141 | 142 | FieldInfo field = t.GetField(name, bindingFlags); if (field != null) 143 | { 144 | return field; 145 | } 146 | 147 | if (t.BaseType != null) 148 | { 149 | return TryGetFieldInfoRecursive(t.BaseType, name); 150 | } 151 | 152 | return null; 153 | } 154 | #pragma warning disable CS0168 155 | catch (Exception e) 156 | #pragma warning restore CS0168 157 | { 158 | Debug.LogError($"{t.FullName} field name: {name} did not work"); 159 | throw; 160 | } 161 | 162 | } 163 | 164 | } 165 | } -------------------------------------------------------------------------------- /Editor/ExtensionsMethods.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9a0957351e7d4d53bc43e843af92ef5a 3 | timeCreated: 1681376579 -------------------------------------------------------------------------------- /Editor/PrefabUsageValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Triband.Validation.Runtime; 4 | using UnityEditor; 5 | using UnityEditor.SceneManagement; 6 | using UnityEngine; 7 | using System.Collections.Generic; 8 | 9 | namespace Triband.Validation.Editor 10 | { 11 | public class PrefabUsageValidator : Validator 12 | { 13 | readonly AssetDependencyPathValidator[] _rules; 14 | 15 | public PrefabUsageValidator() 16 | { 17 | _rules = TypeCache.GetTypesDerivedFrom().Where(t => !t.IsAbstract) 18 | .Select(Activator.CreateInstance).Cast().ToArray(); 19 | } 20 | 21 | protected override void Validate(GameObject gameObject, IValidationContext context) 22 | { 23 | if (PrefabUtility.IsAnyPrefabInstanceRoot(gameObject)) 24 | { 25 | string parentPath; 26 | 27 | if (context.isSceneObject) 28 | { 29 | parentPath = gameObject.scene.path; 30 | } 31 | else 32 | { 33 | PrefabStage prefabStage = PrefabStageUtility.GetPrefabStage(gameObject); 34 | 35 | if (prefabStage != null) 36 | { 37 | parentPath = prefabStage.assetPath; 38 | } 39 | else 40 | { 41 | parentPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject.transform); 42 | } 43 | 44 | } 45 | CheckUsageForPrefabAndParentPrefabs(gameObject, context, parentPath, new HashSet()); 46 | } 47 | } 48 | 49 | //The prefab we're checking might be the child prefab of another prefab. We keep calling 50 | //PrefabUtility.GetCorrespondingObjectFromSource to get all parents of our prefab to be sure 51 | void CheckUsageForPrefabAndParentPrefabs(GameObject gameObject, IValidationContext context, string parentPath, HashSet alreadyValidatedPrefabPaths) 52 | { 53 | string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject); 54 | 55 | if (!alreadyValidatedPrefabPaths.Contains(prefabPath)) 56 | { 57 | foreach (AssetDependencyPathValidator pathRule in _rules) 58 | { 59 | pathRule.ValidatePath(parentPath, prefabPath, context); 60 | } 61 | 62 | alreadyValidatedPrefabPaths.Add(prefabPath); 63 | } 64 | 65 | var source = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); 66 | if (source != gameObject && source != null) 67 | { 68 | CheckUsageForPrefabAndParentPrefabs(source, context, parentPath, alreadyValidatedPrefabPaths); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Editor/PrefabUsageValidation.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8e478d5bfa1e85048b93d9159118fc7e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/SceneValidator.cs: -------------------------------------------------------------------------------- 1 | using Triband.Validation.Runtime; 2 | 3 | namespace Triband.Validation.Editor 4 | { 5 | public abstract class SceneValidator 6 | { 7 | public abstract void DoValidation(IValidationContext context); 8 | } 9 | } -------------------------------------------------------------------------------- /Editor/SceneValidator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e06b910afa2405f95d99ee39ca6e991 3 | timeCreated: 1675850793 -------------------------------------------------------------------------------- /Editor/SerializedFieldValidationHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using JetBrains.Annotations; 5 | using Triband.Validation.Runtime; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | namespace Triband.Validation.Editor 10 | { 11 | [UsedImplicitly] 12 | public class SerializedFieldValidationHandler : Validator 13 | { 14 | internal class PropertyData 15 | { 16 | private Attribute[] _attributes; 17 | public readonly List validator; 18 | 19 | public PropertyData(Attribute[] attributes, List compatibleValidators) 20 | { 21 | _attributes = attributes; 22 | validator = compatibleValidators; 23 | } 24 | 25 | public Attribute[] attributes => _attributes; 26 | 27 | public bool TryGetAttribute(out T result) where T : Attribute 28 | { 29 | foreach (var attribute in _attributes) 30 | { 31 | if (attribute is T cast) 32 | { 33 | result = cast; 34 | return true; 35 | } 36 | } 37 | 38 | result = default; 39 | return false; 40 | } 41 | } 42 | 43 | private readonly SerializedFieldValidator[] _validationRules; 44 | 45 | private Dictionary> _cachedPropertyData = new Dictionary>(); 46 | 47 | public SerializedFieldValidationHandler() 48 | { 49 | _validationRules = TypeCache.GetTypesDerivedFrom() 50 | .Select(Activator.CreateInstance).Cast().ToArray(); 51 | } 52 | 53 | protected override void Validate(Component instance, IValidationContext context) 54 | { 55 | if (_validationRules.Length == 0) return; 56 | 57 | var componentType = instance.GetType(); 58 | if (!_cachedPropertyData.TryGetValue(componentType, out var cachedPropertyData)) 59 | { 60 | cachedPropertyData = new Dictionary(); 61 | _cachedPropertyData.Add(componentType, cachedPropertyData); 62 | } 63 | 64 | var serializeObject = new SerializedObject(instance); 65 | 66 | var iterator = serializeObject.GetIterator(); 67 | do 68 | { 69 | HandleProperty(context, instance, iterator, cachedPropertyData,componentType); 70 | } while (iterator.NextVisible(true)); 71 | } 72 | 73 | private void HandleProperty(IValidationContext context, Component component, SerializedProperty property, Dictionary cachedPropertyData, Type componentType) 74 | { 75 | if (!cachedPropertyData.TryGetValue((property.propertyPath), out PropertyData propertyData)) 76 | { 77 | var attributes = property.GetAttributes(true); 78 | List compatibleValidators = new List(); 79 | foreach (var referenceValidator in _validationRules) 80 | { 81 | if (referenceValidator.CanValidateProperty(componentType, property.propertyPath, 82 | property.propertyType, null, attributes)) 83 | { 84 | compatibleValidators.Add(referenceValidator); 85 | } 86 | } 87 | 88 | propertyData = new PropertyData(attributes, compatibleValidators); 89 | cachedPropertyData.Add(property.propertyPath, propertyData); 90 | } 91 | 92 | foreach (var validationRule in propertyData.validator) 93 | { 94 | validationRule.ValidateProperty(context, property, component, propertyData.attributes); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Editor/SerializedFieldValidationHandler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a100c18e00674aea8751d26a8e9db42b 3 | timeCreated: 1679072335 -------------------------------------------------------------------------------- /Editor/SerializedFieldValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Triband.Validation.Runtime; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Triband.Validation.Editor 7 | { 8 | public abstract class SerializedFieldValidator 9 | { 10 | /// 11 | /// Evaluate if a field can be evaluated at all. The result of this will be cached and 12 | /// only called once per field and the result is cached 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | public virtual bool CanValidateProperty(Type targetType, string propertyPath, 21 | SerializedPropertyType propertyType, Type objectReferenceType, Attribute[] attributes) 22 | { 23 | return true; 24 | } 25 | 26 | public abstract void ValidateProperty(IValidationContext context, SerializedProperty property, 27 | Component component, Attribute[] attributes); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Editor/SerializedFieldValidator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8fd2d0288301463c87721c59ff54997b 3 | timeCreated: 1679072925 -------------------------------------------------------------------------------- /Editor/SourceCodeParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using Triband.Validation.Editor.Data; 5 | 6 | namespace Triband.Validation.Editor 7 | { 8 | sealed class SourceCodeParser : IDisposable 9 | { 10 | Dictionary _cachedArguments; 11 | 12 | internal SourceCodeParser() 13 | { 14 | _cachedArguments = new Dictionary(); 15 | } 16 | 17 | public void Dispose() 18 | { 19 | _cachedArguments = null; 20 | } 21 | 22 | internal static string GetFileNameFromPath(string filePath) 23 | { 24 | var fileSplit = filePath.Split('\\'); 25 | return fileSplit[fileSplit.Length - 1].Replace(".cs", ""); 26 | } 27 | 28 | internal string[] ParseArguments(int argumentCount, string filePath, int lineNumber, out SourceInfo sourceInfo, [CallerMemberName] string callerMemberName = "") 29 | { 30 | try 31 | { 32 | sourceInfo = new SourceInfo(filePath, lineNumber); 33 | 34 | string[] result; 35 | 36 | if (_cachedArguments.TryGetValue(sourceInfo, out result)) 37 | { 38 | return result; 39 | } 40 | 41 | // Parse line one frame down 42 | var line = GetRemainingCodeInFile(filePath, callerMemberName, lineNumber); 43 | 44 | // Read arguments 45 | // Use first and last index to preserve internal function calls 46 | var firstIndex = line.IndexOf('(') + 1; 47 | 48 | int lastIndex = firstIndex; 49 | int depth = 1; 50 | for (int i = firstIndex; i < line.Length; i++) 51 | { 52 | if (line[i] == '(') 53 | { 54 | depth++; 55 | } 56 | 57 | if (line[i] == ')') 58 | { 59 | depth--; 60 | } 61 | 62 | if (depth == 0) 63 | { 64 | lastIndex = i; 65 | break; 66 | } 67 | } 68 | 69 | var argString = line.Substring(firstIndex, lastIndex - firstIndex).Replace(" ", ""); 70 | var args = argString.Split(','); 71 | 72 | if (args.Length < argumentCount) 73 | { 74 | throw new ArgumentException($"Unable to parse {argumentCount} arguments from {filePath}:{lineNumber}"); 75 | } 76 | 77 | result = new string[argumentCount]; 78 | for (int i = 0; i < argumentCount; i++) 79 | { 80 | result[i] = args[i]; 81 | } 82 | 83 | _cachedArguments.Add(sourceInfo, result); 84 | 85 | return result; 86 | } 87 | #pragma warning disable CS0168 88 | catch (Exception e) 89 | #pragma warning restore CS0168 90 | { 91 | sourceInfo = new SourceInfo("Error", 0); 92 | return new[] {"Error"}; 93 | } 94 | } 95 | 96 | string GetRemainingCodeInFile(string filePath, string memberName, int lineNumber) 97 | { 98 | // Read up to target line 99 | var file = new System.IO.StreamReader(filePath); 100 | for (int i = 0; i < lineNumber - 1; i++) 101 | file.ReadLine(); 102 | 103 | // Read requested line 104 | var remainingText = file.ReadToEnd(); 105 | 106 | file.Close(); 107 | 108 | remainingText = remainingText.Replace("\r\n", ""); 109 | remainingText = remainingText.Replace("\n", ""); 110 | 111 | var indexOfMemberName = remainingText.IndexOf(memberName, StringComparison.InvariantCulture); 112 | 113 | remainingText = remainingText.Substring(indexOfMemberName + memberName.Length); 114 | 115 | return remainingText; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Editor/SourceCodeParser.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a2a8e9f387e5eba4cab7632318fc9dc5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Triband.Validation.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Triband.Validation.Editor", 3 | "rootNamespace": "Triband.Validation.Editor", 4 | "references": [ 5 | "Triband.Validation.Runtime", 6 | "Unity.EditorCoroutines.Editor" 7 | ], 8 | "includePlatforms": [ 9 | "Editor" 10 | ], 11 | "excludePlatforms": [], 12 | "allowUnsafeCode": false, 13 | "overrideReferences": true, 14 | "precompiledReferences": [], 15 | "autoReferenced": true, 16 | "defineConstraints": [], 17 | "versionDefines": [], 18 | "noEngineReferences": false 19 | } -------------------------------------------------------------------------------- /Editor/Triband.Validation.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4e42ecf4a3c3cf6469d7330005b10622 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/UI.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 292fe09dbda05394a821c8efbc64ebb6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/UI/ValidationOverlay.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Triband.Validation.Editor.Data; 4 | using Triband.Validation.Runtime; 5 | using UnityEditor; 6 | using UnityEditor.Overlays; 7 | using UnityEditor.SceneManagement; 8 | using UnityEngine; 9 | using UnityEngine.SceneManagement; 10 | using UnityEngine.UIElements; 11 | 12 | namespace Triband.Validation.Editor.UI 13 | { 14 | [Overlay(typeof(SceneView), "Validation", true)] 15 | public class ValidationOverlay : Overlay 16 | { 17 | #region Private Fields 18 | 19 | private List _situationNames; 20 | private DropdownField _dropdownField; 21 | private VisualElement _root; 22 | private TextElement _infoText; 23 | private Button _openValidationWindowButton; 24 | private IssueCollection _issueCollection; 25 | private VisualElement _validationRunningPanel; 26 | private VisualElement _validationResultsPanel; 27 | 28 | #endregion 29 | 30 | #region Public methods 31 | 32 | public override VisualElement CreatePanelContent() 33 | { 34 | _root = new VisualElement(); 35 | 36 | _validationRunningPanel = new VisualElement(); 37 | _validationRunningPanel.Add(new Label("Validating...")); 38 | _validationRunningPanel.visible = false; 39 | _root.Add(_validationRunningPanel); 40 | _validationResultsPanel = new VisualElement(); 41 | _root.Add(_validationResultsPanel); 42 | 43 | _infoText = new TextElement 44 | { 45 | text = "No results", 46 | style = { color = new StyleColor(Color.grey) } 47 | }; 48 | _validationResultsPanel.Add(_infoText); 49 | 50 | var button = new Button { text = "Update" }; 51 | button.clicked += StartValidation; 52 | _validationResultsPanel.Add(button); 53 | 54 | _openValidationWindowButton = new Button { text = "Open Validation Window" }; 55 | _openValidationWindowButton.clicked += () => 56 | { 57 | var validationWindow = EditorWindow.GetWindow(); 58 | validationWindow.Show(); 59 | }; 60 | 61 | UpdateEventCallbacks(displayed); 62 | 63 | UpdateFromResult(); 64 | ValidationManager.RunValidation(); 65 | 66 | return _root; 67 | } 68 | 69 | private static void StartValidation() 70 | { 71 | ValidationManager.RunValidation(); 72 | } 73 | 74 | public override void OnCreated() 75 | { 76 | displayedChanged += UpdateEventCallbacks; 77 | UpdateEventCallbacks(displayed); 78 | } 79 | 80 | void UpdateEventCallbacks(bool isVisible) 81 | { 82 | if (isVisible) 83 | { 84 | EditorSceneManager.sceneSaved += OnSceneSaved; 85 | EditorSceneManager.activeSceneChangedInEditMode += OnSceneChanged; 86 | 87 | ValidationManager.onValidationStarted += OnValidationStarted; 88 | ValidationManager.onValidationFinished += UpdateFromResult; 89 | 90 | PrefabStage.prefabStageOpened += PrefabStageChanged; 91 | PrefabStage.prefabStageClosing += PrefabStageChanged; 92 | 93 | EditorApplication.playModeStateChanged += OnPlayModeChanged; 94 | } 95 | else 96 | { 97 | EditorSceneManager.sceneSaved -= OnSceneSaved; 98 | EditorSceneManager.activeSceneChangedInEditMode -= OnSceneChanged; 99 | 100 | ValidationManager.onValidationFinished -= UpdateFromResult; 101 | ValidationManager.onValidationStarted -= OnValidationStarted; 102 | 103 | PrefabStage.prefabStageOpened -= PrefabStageChanged; 104 | PrefabStage.prefabStageClosing -= PrefabStageChanged; 105 | 106 | EditorApplication.playModeStateChanged -= OnPlayModeChanged; 107 | } 108 | } 109 | 110 | void OnPlayModeChanged(PlayModeStateChange obj) 111 | { 112 | if (obj == PlayModeStateChange.ExitingEditMode || obj == PlayModeStateChange.EnteredPlayMode) 113 | { 114 | _root.SetEnabled(false); 115 | } 116 | else 117 | { 118 | _root.SetEnabled(true); 119 | } 120 | } 121 | 122 | void PrefabStageChanged(PrefabStage _) 123 | { 124 | StartValidation(); 125 | } 126 | 127 | 128 | private void OnValidationStarted() 129 | { 130 | _validationResultsPanel.visible = false; 131 | _validationRunningPanel.visible = true; 132 | } 133 | 134 | #endregion 135 | 136 | #region Private methods 137 | 138 | private void OnSceneChanged(Scene _, Scene newScene) 139 | { 140 | //When running Unit Tests, Unity first loads a completely empty scene 141 | //Running validation in this one causes crashes 142 | if (newScene.name == string.Empty && newScene.rootCount == 0) 143 | { 144 | return; 145 | } 146 | 147 | if (ValidationManager.isRunningUnitTest) 148 | { 149 | return; 150 | } 151 | 152 | StartValidation(); 153 | } 154 | 155 | private void OnSceneSaved(Scene _) 156 | { 157 | StartValidation(); 158 | } 159 | 160 | private void UpdateFromResult() 161 | { 162 | _validationResultsPanel.visible = true; 163 | _validationRunningPanel.visible = false; 164 | 165 | if (!displayed) 166 | { 167 | return; 168 | } 169 | 170 | _issueCollection = ValidationManager.currentIssueCollection; 171 | 172 | if (_issueCollection.issues.Count > 0) 173 | { 174 | var errorCount = 175 | _issueCollection.issues.Count(issue => issue.severity == ValidationSeverity.Error); 176 | 177 | var warningCount = 178 | _issueCollection.issues.Count(issue => issue.severity == ValidationSeverity.Warning); 179 | 180 | _infoText.enableRichText = true; 181 | _infoText.text = string.Empty; 182 | 183 | if (errorCount > 0) 184 | { 185 | _infoText.text += $"{errorCount} Errors Found In Scene"; 186 | if (warningCount > 0) 187 | { 188 | _infoText.text += "\n"; 189 | } 190 | } 191 | 192 | if (warningCount > 0) 193 | { 194 | _infoText.text += $"{warningCount} Warnings Found In Scene"; 195 | } 196 | 197 | if (!_validationResultsPanel.Contains(_openValidationWindowButton)) 198 | { 199 | _validationResultsPanel.Add(_openValidationWindowButton); 200 | } 201 | } 202 | else 203 | { 204 | _infoText.text = "No Issues Found"; 205 | _infoText.style.color = new StyleColor(Color.green); 206 | 207 | if (_validationResultsPanel.Contains(_openValidationWindowButton)) 208 | { 209 | _validationResultsPanel.Remove(_openValidationWindowButton); 210 | } 211 | } 212 | } 213 | 214 | #endregion 215 | } 216 | } -------------------------------------------------------------------------------- /Editor/UI/ValidationOverlay.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f075daf09a794a77b6492d9ebb895bff 3 | timeCreated: 1651844371 -------------------------------------------------------------------------------- /Editor/UI/ValidationResult.uxml: -------------------------------------------------------------------------------- 1 | 2 |