using UnityEngine; using UnityEditor; using System.Linq; using System.Reflection; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.SceneManagement; using UnityEngine.SceneManagement; using EditorAttributes.Editor.Utility; #if HAS_ADDRESSABLES_PACKAGE using UnityEditor.AddressableAssets; #endif namespace EditorAttributes.Editor { [InitializeOnLoad] public class EditorValidation : IPreprocessBuildWithReport { private static int BUILD_KILLERS; public int callbackOrder => 0; static EditorValidation() { } public void OnPreprocessBuild(BuildReport report) { BUILD_KILLERS = 0; if (!EditorAttributesSettings.instance.disableBuildValidation) ValidateAll(); if (BUILD_KILLERS != 0) throw new BuildFailedException("Validation Failed"); } /// /// Validates every asset and scene in the project /// [MenuItem("Tools/EditorValidation/Validate All", priority = 0)] public static void ValidateAll() { ValidateAllAssets(); ValidateAllScenes(); } /// /// Validates all scenes in the build /// [MenuItem("Tools/EditorValidation/Validate Scenes", priority = 2)] public static void ValidateAllScenes() { int failedValidations = 0; int successfulValidations = 0; var sceneGuids = AssetDatabase.FindAssets("t:Scene"); var previouslyOpenedScenes = GetAllOpenedScenes(); foreach (var sceneGuid in sceneGuids) { string scenePath = AssetDatabase.GUIDToAssetPath(sceneGuid); if (IsPackageAsset(scenePath)) continue; if (SceneUtility.GetBuildIndexByScenePath(scenePath) == -1 && !IsAddressable(sceneGuid)) continue; var openedScene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); ValidateScene(openedScene, ref failedValidations, ref successfulValidations); if (previouslyOpenedScenes.All((scene) => scene.path != scenePath)) EditorSceneManager.CloseScene(openedScene, true); } Debug.Log($"Scenes Validated: (Failed: {failedValidations}, Succeeded: {successfulValidations}, Total: {failedValidations + successfulValidations})"); } private static bool IsAddressable(string guid) { #if HAS_ADDRESSABLES_PACKAGE var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) return false; foreach (var group in settings.groups) { if (group == null || group.entries.Count == 0) continue; foreach (var entry in group.entries) { if (entry.guid == guid) return true; } } #endif return false; } /// /// Validates all scenes currently open /// [MenuItem("Tools/EditorValidation/Validate Open Scenes", priority = 3)] public static void ValidateOpenScenes() { int failedValidations = 0; int successfulValidations = 0; foreach (var openedScene in GetAllOpenedScenes()) ValidateScene(openedScene, ref failedValidations, ref successfulValidations); Debug.Log($"Scenes Validated: (Failed: {failedValidations}, Succeeded: {successfulValidations}, Total: {failedValidations + successfulValidations})"); } /// /// Validates all assets in the project /// [MenuItem("Tools/EditorValidation/Validate Assets", priority = 1)] public static void ValidateAllAssets() { int failedValidations = 0; int successfulValidations = 0; var prefabGuids = AssetDatabase.FindAssets("t:Prefab"); foreach (var prefabGuid in prefabGuids) { string prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid); if (IsPackageAsset(prefabPath)) continue; var prefab = AssetDatabase.LoadAssetAtPath(prefabPath); ValidateComponents(prefab.GetComponentsInChildren(true), ref failedValidations, ref successfulValidations); } var scriptableObjectGuids = AssetDatabase.FindAssets("t:ScriptableObject"); foreach (var scriptableObjectGuid in scriptableObjectGuids) { string scriptableObjectPath = AssetDatabase.GUIDToAssetPath(scriptableObjectGuid); if (IsPackageAsset(scriptableObjectPath)) continue; var scriptableObject = AssetDatabase.LoadAssetAtPath(scriptableObjectPath); Validate(scriptableObject, ref failedValidations, ref successfulValidations); } Debug.Log($"Assets Validated: (Failed: {failedValidations}, Succeeded: {successfulValidations}, Total: {failedValidations + successfulValidations})"); } /// /// Validates all fields marked for validation with an attribute /// /// The target object to validate /// The amount of validations that failed /// The amount of validations that succeded public static void Validate(Object targetObject, ref int failedValidations, ref int successfulValidations) { var type = targetObject.GetType(); var fields = type.GetFields(ReflectionUtility.BINDING_FLAGS); foreach (var field in fields) { string validationMessage = $"Validation failed on {type.Name}.{field.Name} in {targetObject.name}: "; var requiredAttribute = field.GetCustomAttribute(); if (requiredAttribute != null && requiredAttribute.ThrowValidationError) { var fieldValue = field.GetValue(targetObject); if (IsNotValid(fieldValue)) { if (requiredAttribute.BuildKiller) { BUILD_KILLERS++; validationMessage = "(Build Killer) " + validationMessage; } Debug.LogError(validationMessage + "Field not assigned", targetObject); failedValidations++; } else { successfulValidations++; } } var validateAttribute = field.GetCustomAttribute(); if (validateAttribute != null) { var conditionalMember = ReflectionUtility.GetValidMemberInfo(validateAttribute.ConditionName, targetObject); if (EvaluateCondition(conditionalMember, targetObject, out ValidationCheck customCheck)) { string customMessage = customCheck == null ? validateAttribute.ValidationMessage : customCheck.ValidationMessage; bool isBuildKiller = customCheck == null ? validateAttribute.BuildKiller : customCheck.KillBuild; var severety = customCheck == null ? validateAttribute.Severety : customCheck.Severety; if (isBuildKiller) { BUILD_KILLERS++; validationMessage = "(Build Killer) " + validationMessage; } switch (severety) { case MessageMode.None: case MessageMode.Log: Debug.Log(validationMessage + customMessage, targetObject); break; case MessageMode.Warning: Debug.LogWarning(validationMessage + customMessage, targetObject); break; case MessageMode.Error: Debug.LogError(validationMessage + customMessage, targetObject); break; } failedValidations++; } else { successfulValidations++; } } } } /// /// Checks to see if an asset is inside the Packages folder /// /// The path of the asset /// True if the asset is inside the packages folder public static bool IsPackageAsset(string assetPath) => assetPath.StartsWith("Packages/"); /// /// Returns an array of all the Scenes currently open in the hierarchy /// /// Array of Scenes in the Hierarchy public static Scene[] GetAllOpenedScenes() { var array = new Scene[SceneManager.sceneCount]; for (int i = 0; i < SceneManager.sceneCount; i++) array[i] = SceneManager.GetSceneAt(i); return array; } private static void ValidateScene(Scene scene, ref int failedValidations, ref int successfulValidations) { var rootObjects = scene.GetRootGameObjects(); foreach (var rootObject in rootObjects) { // Check all children recursively var childTransforms = rootObject.GetComponentsInChildren(true); foreach (var childTransform in childTransforms) ValidateComponents(childTransform.gameObject.GetComponents(), ref failedValidations, ref successfulValidations); } } private static void ValidateComponents(Component[] components, ref int failedValidations, ref int successfulValidations) { foreach (var component in components) { if (component == null) continue; Validate(component, ref failedValidations, ref successfulValidations); } } private static bool EvaluateCondition(MemberInfo memberInfo, object targetObject, out ValidationCheck customValidationCheck) { var memberInfoType = ReflectionUtility.GetMemberInfoType(memberInfo); string errorMessage = $"Couldn't validate condition, check for any error box messages on {targetObject}"; customValidationCheck = null; if (memberInfoType == null) { Debug.LogError(errorMessage, (Object)targetObject); return true; } if (memberInfoType == typeof(bool)) { var memberInfoValue = ReflectionUtility.GetMemberInfoValue(memberInfo, targetObject); if (memberInfoValue == null) return false; return (bool)memberInfoValue; } else if (memberInfoType == typeof(ValidationCheck)) { if (ReflectionUtility.GetMemberInfoValue(memberInfo, targetObject) is not ValidationCheck memberInfoValue) return false; customValidationCheck = memberInfoValue; return !memberInfoValue.PassedCheck; } Debug.LogError(errorMessage, (Object)targetObject); return true; } private static bool IsNotValid(object fieldValue) => fieldValue == null || fieldValue.Equals(null); } }