diff --git a/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs index a41dbc3..cacd52c 100644 --- a/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs +++ b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs @@ -10,19 +10,32 @@ namespace Module.ProjectValidator.Editor { static EditorProjectValidatorHierarchy() { +#if UNITY_6000_4_OR_NEWER + EditorApplication.hierarchyWindowItemByEntityIdOnGUI -= OnHierarchyWindowItemByEntityIdOnGUI; + EditorApplication.hierarchyWindowItemByEntityIdOnGUI += OnHierarchyWindowItemByEntityIdOnGUI; +#else EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyWindowItemOnGUI; EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemOnGUI; +#endif EditorSceneManager.sceneOpened -= OnSceneOpened; EditorSceneManager.sceneOpened += OnSceneOpened; } +#if UNITY_6000_4_OR_NEWER + private static void OnHierarchyWindowItemByEntityIdOnGUI(EntityId entityId, Rect selectionRect) + { + if (Report.HasActive && Report.Active.TryGetSeverityFor(entityId, out var instance) && instance.Severity != EValidatorSeverity.Valid) + EditorIconUtility.Draw(new Rect(selectionRect.x, selectionRect.y, selectionRect.height, selectionRect.height), instance.Severity, instance.IsRedirect); + } +#else private static void OnHierarchyWindowItemOnGUI(int instanceID, Rect selectionRect) { if (Report.HasActive && Report.Active.TryGetSeverityFor(instanceID, out var instance) && instance.Severity != EValidatorSeverity.Valid) EditorIconUtility.Draw(new Rect(selectionRect.x, selectionRect.y, selectionRect.height, selectionRect.height), instance.Severity, instance.IsRedirect); } - +#endif + private static void OnSceneOpened(Scene scene, OpenSceneMode mode) { if (Report.HasActive && ProjectValidatorUtility.IsValidForRun()) diff --git a/Editor/Icons/editor_project_validator_settings.png b/Editor/Icons/editor_project_validator_settings.png new file mode 100644 index 0000000..f938577 Binary files /dev/null and b/Editor/Icons/editor_project_validator_settings.png differ diff --git a/Editor/Icons/editor_project_validator_settings.png.meta b/Editor/Icons/editor_project_validator_settings.png.meta new file mode 100644 index 0000000..524687d --- /dev/null +++ b/Editor/Icons/editor_project_validator_settings.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: a6c61d5fbd310894d8159ba6af32d7e3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Objects/Report.cs b/Editor/Objects/Report.cs index aa6d136..5b68f9f 100644 --- a/Editor/Objects/Report.cs +++ b/Editor/Objects/Report.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using UnityEditor; +using UnityEngine; namespace Module.ProjectValidator.Editor { @@ -11,7 +12,12 @@ namespace Module.ProjectValidator.Editor public readonly List Entries = new(); private readonly Dictionary _assetToSeverityMapping = new(); + +#if UNITY_6000_4_OR_NEWER + private readonly Dictionary _instanceToSeverityMapping = new(); +#else private readonly Dictionary _instanceToSeverityMapping = new(); +#endif public void Add(GUID assetGuid, string relativePath, string fieldPath, Attribute attribute, EValidatorSeverity severity, string message) { @@ -64,7 +70,17 @@ namespace Module.ProjectValidator.Editor mapping = new MappingEntry(); return false; } - + +#if UNITY_6000_4_OR_NEWER + public bool TryGetSeverityFor(EntityId entityId, out MappingEntry mapping) + { + if (_instanceToSeverityMapping.TryGetValue(entityId, out mapping)) + return true; + + mapping = new MappingEntry(); + return false; + } +#else public bool TryGetSeverityFor(int instanceId, out MappingEntry mapping) { if (_instanceToSeverityMapping.TryGetValue(instanceId, out mapping)) @@ -73,6 +89,7 @@ namespace Module.ProjectValidator.Editor mapping = new MappingEntry(); return false; } +#endif public bool TryGetSeverityFor(GUID assetGuid, string relativePath, out MappingEntry mapping) { diff --git a/Editor/Objects/TypeTree.cs b/Editor/Objects/TypeTree.cs index 5b69e0c..4cddc30 100644 --- a/Editor/Objects/TypeTree.cs +++ b/Editor/Objects/TypeTree.cs @@ -31,9 +31,16 @@ namespace Module.ProjectValidator.Editor if (!IsFieldSerializable(fi)) continue; - var attributes = fi.GetCustomAttributes(); + var fieldAttributes = fi.GetCustomAttributes(); + var fieldTypeAttributes = fi.FieldType.GetCustomAttributes(); - foreach (var attribute in attributes) + foreach (var attribute in fieldAttributes) + { + if (validatorList.TryGetAttributeValidator(attribute.GetType(), out var validator)) + entry.AddField(fi, attribute, validator); + } + + foreach (var attribute in fieldTypeAttributes) { if (validatorList.TryGetAttributeValidator(attribute.GetType(), out var validator)) entry.AddField(fi, attribute, validator); @@ -164,7 +171,7 @@ namespace Module.ProjectValidator.Editor return Components == null && Fields == null && Entries == null; } } - + public sealed class ValidatorField { public readonly FieldInfo FieldInfo; diff --git a/Editor/Objects/ValidatorList.cs b/Editor/Objects/ValidatorList.cs index 0f498fa..39221ff 100644 --- a/Editor/Objects/ValidatorList.cs +++ b/Editor/Objects/ValidatorList.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.Serialization; using UnityEngine; @@ -11,6 +12,7 @@ namespace Module.ProjectValidator.Editor private readonly Dictionary _attributeValidators = new(); private readonly Dictionary> _componentValidators = new(); public readonly List GameObjectValidators = new(); + public readonly Dictionary> AssetValidators = new(); public void AddAttribute(Type type) { @@ -75,6 +77,33 @@ namespace Module.ProjectValidator.Editor Debug.LogException(e); } } + + public void AddAsset(Type type) + { + if (type.IsInterface || type.IsAbstract) + return; + + var typeValidator = type.GetInterfaces().FirstOrDefault(typeInterface => typeInterface.IsGenericType && typeInterface.GetGenericTypeDefinition() == typeof(IAssetValidator<>)); + var componentType = typeValidator?.GetGenericArguments()[0]; + + if (componentType == null) + return; + + try + { + var instance = FormatterServices.GetUninitializedObject(type); + var validator = new AssetValidator(instance); + + if (AssetValidators.TryGetValue(componentType, out var list)) + list.Add(validator); + else + AssetValidators.Add(componentType, new List { validator }); + } + catch (Exception e) + { + Debug.LogException(e); + } + } public bool TryGetAttributeValidator(Type type, out object validatorInstance) { @@ -85,5 +114,17 @@ namespace Module.ProjectValidator.Editor { return _componentValidators.TryGetValue(type, out validatorInstances); } + + public sealed class AssetValidator + { + public readonly object Validator; + public readonly MethodInfo ValidatorMethod; + + public AssetValidator(object validator) + { + Validator = validator; + ValidatorMethod = validator.GetType().GetMethod("Validate"); + } + } } } \ No newline at end of file diff --git a/Editor/Settings/ProjectValidatorSettings.cs b/Editor/Settings/ProjectValidatorSettings.cs index b526db7..ec2893f 100644 --- a/Editor/Settings/ProjectValidatorSettings.cs +++ b/Editor/Settings/ProjectValidatorSettings.cs @@ -1,27 +1,36 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using UnityEditor.UIElements; using UnityEditorInternal; +using UnityEngine.Pool; +using Object = UnityEngine.Object; namespace Module.ProjectValidator.Editor { internal sealed class ProjectValidatorSettings : ScriptableObject { public List assemblies = new(); - + [NonReorderable] + public List validators = new(); + + public const string MenuPath = "Project/Project Validator"; private const string AssetPath = "ProjectSettings/ProjectValidatorSettings.asset"; private const string StyleSheetName = "StyleSheetProjectValidatorSettings"; internal static ProjectValidatorSettings GetOrCreate() { var objects = InternalEditorUtility.LoadSerializedFileAndForget(AssetPath); + ProjectValidatorSettings settings; if (objects.Length != 0) - return (ProjectValidatorSettings)objects[0]; + settings = (ProjectValidatorSettings)objects[0]; + else + settings = CreateInstance(); - var settings = CreateInstance(); + PopulateValidatorList(settings); InternalEditorUtility.SaveToSerializedFileAndForget(new Object[] { settings }, AssetPath, true); return settings; } @@ -29,7 +38,7 @@ namespace Module.ProjectValidator.Editor [SettingsProvider] public static SettingsProvider CreateProvider() { - return new SettingsProvider("Project/Project Validator", SettingsScope.Project) + return new SettingsProvider(MenuPath, SettingsScope.Project) { label = "Project Validator", activateHandler = (_, root) => @@ -37,16 +46,79 @@ namespace Module.ProjectValidator.Editor var settings = GetOrCreate(); var serializedObject = new SerializedObject(settings); var container = new VisualElement { style = { flexDirection = FlexDirection.Column } }; - var propertyField = new PropertyField(serializedObject.FindProperty("assemblies"), "Assemblies"); - propertyField.RegisterCallback>(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); - propertyField.RegisterValueChangeCallback(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); - container.Add(propertyField); + + var assemblyField = new PropertyField(serializedObject.FindProperty(nameof(assemblies)), "Assemblies"); + container.Add(assemblyField); + + var enabledField = new PropertyField(serializedObject.FindProperty(nameof(validators)), "Validators"); + container.Add(enabledField); + root.Add(container); root.Bind(serializedObject); + root.RegisterCallback(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); + root.RegisterCallback(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); root.styleSheets.Add(EditorAssetUtility.LoadFirstAsset(StyleSheetName)); }, keywords = new HashSet(new[] { "Project", "Validator", "Assemblies" }) }; } + + private static void PopulateValidatorList(ProjectValidatorSettings settings) + { + using var pool0 = ListPool.Get(out var list); + using var pool1 = ListPool.Get(out var temp); + + FetchValidatorsOfType(typeof(IAssetValidator<>), list); + FetchValidatorsOfType(typeof(IAttributeValidator<>), list); + FetchValidatorsOfType(typeof(IComponentValidator<>), list); + FetchValidatorsOfType(typeof(IGameObjectValidator), list); + + for (var i = 0; i < settings.validators.Count; i++) + { + temp.Add(settings.validators[i].assemblyQualifiedName); + } + + for (var i = 0; i < list.Count; i++) + { + if (temp.Contains(list[i])) + continue; + + var type = Type.GetType(list[i]); + var name = type != null ? type.Name : "Unknown Type"; + settings.validators.Add(new ValidatorEnabled(name, list[i], true)); + } + + for (var i = temp.Count - 1; i >= 0; i--) + { + if (!list.Contains(temp[i])) + settings.validators.RemoveAt(i); + } + } + + private static void FetchValidatorsOfType(Type type, List typeNames) + { + var types = TypeCache.GetTypesDerivedFrom(type); + + for (var i = 0; i < types.Count; i++) + { + if (!types[i].IsInterface && !types[i].IsAbstract) + typeNames.Add(types[i].AssemblyQualifiedName); + } + } + + [Serializable] + public sealed class ValidatorEnabled + { + public string name; + public string assemblyQualifiedName; + public bool enabled; + + public ValidatorEnabled(string name, string assemblyQualifiedName, bool enabled) + { + this.name = name; + this.assemblyQualifiedName = assemblyQualifiedName; + this.enabled = enabled; + } + } } } \ No newline at end of file diff --git a/Editor/Utilities/EditorAssetUtility.cs b/Editor/Utilities/EditorAssetUtility.cs index be7796a..e3c0ea4 100644 --- a/Editor/Utilities/EditorAssetUtility.cs +++ b/Editor/Utilities/EditorAssetUtility.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; +using Object = UnityEngine.Object; namespace Module.ProjectValidator.Editor { @@ -29,6 +31,22 @@ namespace Module.ProjectValidator.Editor return list.ToArray(); } + public static Object[] LoadAllAssets(Type type) + { + var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{type.Name}"); + var list = new List(guids.Length); + + foreach (var guid in guids) + { + var asset = AssetDatabase.LoadAssetByGUID(guid, type); + + if (asset != null) + list.Add(asset); + } + + return list.ToArray(); + } + internal static GUID GetAssetGuid(Object obj) { var assetPath = string.Empty; diff --git a/Editor/Utilities/ProjectValidatorUtility.cs b/Editor/Utilities/ProjectValidatorUtility.cs index f9f520c..10d6eef 100644 --- a/Editor/Utilities/ProjectValidatorUtility.cs +++ b/Editor/Utilities/ProjectValidatorUtility.cs @@ -37,6 +37,14 @@ namespace Module.ProjectValidator.Editor window.titleContent = new GUIContent("Project Validator"); return window; } + + public static string GetAssetValidatorName(object validator) + { + var str = validator.GetType().Name; + str = str.Replace("AssetValidator", string.Empty); + str = ObjectNames.NicifyVariableName(str); + return str; + } internal static string GetGameObjectValidatorName(IGameObjectValidator validator) { @@ -196,6 +204,93 @@ namespace Module.ProjectValidator.Editor } } +#if UNITY_6000_4_OR_NEWER + internal static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping) + { + dictMapping.Clear(); + using var _ = ListPool.Get(out var rootObjects); + + for (var i = 0; i < SceneManager.sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + + if (!scene.isLoaded) + continue; + + var strAssetGuid = AssetDatabase.AssetPathToGUID(scene.path); + GUID.TryParse(strAssetGuid, out var assetGuid); + scene.GetRootGameObjects(rootObjects); + + for (var j = 0; j < rootObjects.Count; j++) + { + var rootObject = rootObjects[j]; + var relativePath = string.Empty; + RebuildSceneInstanceMapping(report, dictMapping, rootObject, assetGuid, relativePath, true); + } + } + + RebuildForAllParents(dictMapping); + } + + + private static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping, GameObject gameObject, GUID assetGuid, string relativePath, bool initial) + { + var transform = gameObject.transform; + AppendToRelativePath(gameObject, ref relativePath, initial); + + if (report.TryGetSeverityFor(assetGuid, relativePath, out var mapping)) + dictMapping.Add(gameObject.GetEntityId(), new Report.MappingEntry(mapping.Severity, false)); + + for (var i = 0; i < transform.childCount; i++) + { + RebuildSceneInstanceMapping(report, dictMapping, transform.GetChild(i).gameObject, assetGuid, relativePath, false); + } + } + + private static void RebuildForAllParents(Dictionary dictMapping) + { + using var _ = DictionaryPool.Get(out var newMappings); + + foreach (var pair in dictMapping) + { + var obj = EditorUtility.EntityIdToObject(pair.Key); + + if (obj is not GameObject gameObject) + continue; + + var severity = pair.Value.Severity; + var transform = gameObject.transform.parent; + + while (transform != null) + { + gameObject = transform.gameObject; + var entityId = gameObject.GetEntityId(); + + if (dictMapping.TryGetValue(entityId, out var parentMapping)) + { + if (severity < parentMapping.Severity) + severity = parentMapping.Severity; + } + else if (newMappings.TryGetValue(entityId, out var currentMapping)) + { + if (currentMapping.Severity < severity) + newMappings[entityId] = new Report.MappingEntry(severity, true); + } + else + { + newMappings.Add(entityId, new Report.MappingEntry(severity, true)); + } + + transform = transform.parent; + } + } + + foreach (var pair in newMappings) + { + dictMapping.Add(pair.Key, pair.Value); + } + } +#else internal static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping) { dictMapping.Clear(); @@ -223,6 +318,7 @@ namespace Module.ProjectValidator.Editor RebuildForAllParents(dictMapping); } + private static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping, GameObject gameObject, GUID assetGuid, string relativePath, bool initial) { var transform = gameObject.transform; @@ -280,6 +376,7 @@ namespace Module.ProjectValidator.Editor dictMapping.Add(pair.Key, pair.Value); } } +#endif internal static void RefreshUnityWindows() { diff --git a/Editor/ValidatorRunner.cs b/Editor/ValidatorRunner.cs index 3cbfa63..b69797a 100644 --- a/Editor/ValidatorRunner.cs +++ b/Editor/ValidatorRunner.cs @@ -13,7 +13,6 @@ namespace Module.ProjectValidator.Editor { internal static class ValidatorRunner { - private static bool _initialized; private static ValidatorList _validatorList; private static TypeTree _typeTree; @@ -56,21 +55,19 @@ namespace Module.ProjectValidator.Editor private static void Initialize() { - if (_initialized) - return; - var settings = ProjectValidatorSettings.GetOrCreate(); var assemblies = GetAssembliesFrom(settings); - + var enabled = GetEnabledValidators(settings); + _validatorList = new ValidatorList(); _typeTree = new TypeTree(); - FetchAllGameObjectValidators(); - FetchAllComponentValidators(); - FetchAllAttributeValidators(); + FetchAllGameObjectValidators(enabled); + FetchAllComponentValidators(enabled); + FetchAllAttributeValidators(enabled); + FetchAllAssetValidators(enabled); FetchAllTypesWithValidators(assemblies); FetchAllTypesWithValidators(assemblies); - _initialized = true; } private static Assembly[] GetAssembliesFrom(ProjectValidatorSettings settings) @@ -93,33 +90,72 @@ namespace Module.ProjectValidator.Editor return assemblies.ToArray(); } - private static void FetchAllAttributeValidators() + private static HashSet GetEnabledValidators(ProjectValidatorSettings settings) + { + var enabled = new HashSet(settings.validators.Count); + + for (var i = 0; i < settings.validators.Count; i++) + { + try + { + if (!settings.validators[i].enabled) + continue; + + var type = Type.GetType(settings.validators[i].assemblyQualifiedName); + + if (type != null) + enabled.Add(type); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + return enabled; + } + + private static void FetchAllAttributeValidators(HashSet enabled) { var types = TypeCache.GetTypesDerivedFrom(typeof(IAttributeValidator<>)); for (var i = 0; i < types.Count; i++) { - _validatorList.AddAttribute(types[i]); + if (enabled.Contains(types[i])) + _validatorList.AddAttribute(types[i]); } } - private static void FetchAllGameObjectValidators() + private static void FetchAllGameObjectValidators(HashSet enabled) { var types = TypeCache.GetTypesDerivedFrom(typeof(IGameObjectValidator)); for (var i = 0; i < types.Count; i++) { - _validatorList.AddGameObject(types[i]); + if (enabled.Contains(types[i])) + _validatorList.AddGameObject(types[i]); } } - private static void FetchAllComponentValidators() + private static void FetchAllComponentValidators(HashSet enabled) { var types = TypeCache.GetTypesDerivedFrom(typeof(IComponentValidator<>)); for (var i = 0; i < types.Count; i++) { - _validatorList.AddComponent(types[i]); + if (enabled.Contains(types[i])) + _validatorList.AddComponent(types[i]); + } + } + + private static void FetchAllAssetValidators(HashSet enabled) + { + var types = TypeCache.GetTypesDerivedFrom(typeof(IAssetValidator<>)); + + for (var i = 0; i < types.Count; i++) + { + if (enabled.Contains(types[i])) + _validatorList.AddAsset(types[i]); } } @@ -143,12 +179,12 @@ namespace Module.ProjectValidator.Editor for (var i = 0; i < assets.Length; i++) { + var assetPath = AssetDatabase.GetAssetPath(assets[i]); + var scene = SceneManager.GetSceneByPath(assetPath); + var isLoaded = scene.isLoaded; + try { - var assetPath = AssetDatabase.GetAssetPath(assets[i]); - var scene = SceneManager.GetSceneByPath(assetPath); - var isLoaded = scene.isLoaded; - if (!isLoaded) scene = EditorSceneManager.OpenScene(assetPath, OpenSceneMode.Additive); @@ -158,20 +194,41 @@ namespace Module.ProjectValidator.Editor { ValidateGameObject(rootObjects[j], string.Empty, report, true); } - - if (!isLoaded) - EditorSceneManager.CloseScene(scene, true); } catch (Exception e) { Debug.LogException(e); } + finally + { + if (!isLoaded && scene.isLoaded) + EditorSceneManager.CloseScene(scene, true); + } } } private static void ValidateAllAssets(Report report) { ValidateAssetsBytype(report); + + foreach (var pair in _validatorList.AssetValidators) + { + var assets = EditorAssetUtility.LoadAllAssets(pair.Key); + + for (var i = 0; i < assets.Length; i++) + { + try + { + var assetPath = AssetDatabase.GetAssetPath(assets[i]); + var assetGuid = AssetDatabase.GUIDFromAssetPath(assetPath); + ValidateAsset(assets[i], assetGuid, assetPath, pair.Value, report); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } } private static void ValidateAllPrefabs(Report report) @@ -253,6 +310,29 @@ namespace Module.ProjectValidator.Editor Validate(assetGuid, relativePath, components[i], report); } } + + private static void ValidateAsset(UnityEngine.Object obj, GUID assetGuid, string relativePath, List validators, Report report) + { + using var _ = ListPool.Get(out var results); + + for (var i = 0; i < validators.Count; i++) + { + results.Clear(); + var validator = validators[i]; + validator.ValidatorMethod.Invoke(validator.Validator, new object[] { obj, results }); + + for (var j = 0; j < results.Count; j++) + { + var result = results[j]; + + if (result.Severity == EValidatorSeverity.Valid) + continue; + + var validatorName = ProjectValidatorUtility.GetAssetValidatorName(validator.Validator); + report.Add(assetGuid, relativePath, string.Empty, validatorName, result.Severity, result.Message); + } + } + } private static void ValidateChildren(GameObject gameObject, string relativePath, Report report) { @@ -343,8 +423,18 @@ namespace Module.ProjectValidator.Editor var fieldPath = parentFieldPath; ProjectValidatorUtility.AppendToFieldPath(e.FieldInfo, ref fieldPath); - - if (value is IEnumerable ie) + + if (value is Array arr) + { + for (var j = 0; j < arr.Length; j++) + { + var eObj = arr.GetValue(j); + var fieldPathArrElement = fieldPath; + ProjectValidatorUtility.AppendToFieldPath(j, ref fieldPathArrElement); + Validate(assetGuid, relativePath, fieldPathArrElement, eObj, e.Entry, report); + } + } + else if (value is IEnumerable ie) { var idx = 0; diff --git a/Editor/Validators/Assets.meta b/Editor/Validators/Assets.meta new file mode 100644 index 0000000..a1c08b2 --- /dev/null +++ b/Editor/Validators/Assets.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ddbf30cd5a674751be0c125c1f4e917b +timeCreated: 1779623970 \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialShader.cs b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs new file mode 100644 index 0000000..f6e072f --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AssetValidatorMaterialShader : IAssetValidator + { + public void Validate(Material obj, List results) + { + if (obj.shader == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, "Shader is Null")); + else if (!IsCompatible(obj.shader)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"Shader '{obj.shader.name}' is not compatible with render pipeline")); + else if (!obj.shader.isSupported) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"Shader '{obj.shader.name}' is not supported")); + else if (ShaderUtil.ShaderHasError(obj.shader)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Shader '{obj.shader.name}' has compile errors")); + } + + private static bool IsCompatible(Shader shader) + { + var pipeline = GraphicsSettings.currentRenderPipeline; + + if (pipeline == null) + return true; + + var tagSearch = new ShaderTagId("RenderPipeline"); + var tagPipeline = new ShaderTagId(pipeline.renderPipelineShaderTag); + var hasKeyword = false; + + for (var i = 0; i < shader.passCount; i++) + { + var tagPass = shader.FindPassTagValue(i, tagSearch); + + if (tagPass != ShaderTagId.none) + hasKeyword = true; + + if (tagPass == tagPipeline) + return true; + } + + return !hasKeyword; + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialShader.cs.meta b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs.meta new file mode 100644 index 0000000..a1eceb1 --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2218d247daff44bf84629756b63ea650 +timeCreated: 1779623986 \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs new file mode 100644 index 0000000..37097fa --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AssetValidatorMaterialTexture : IAssetValidator + { + public void Validate(Material obj, List results) + { + if (obj.shader == null) + return; + + var count = obj.shader.GetPropertyCount(); + + for (var i = 0; i < count; i++) + { + var propertyType = obj.shader.GetPropertyType(i); + var propertyFlags = obj.shader.GetPropertyFlags(i); + + if (propertyType != ShaderPropertyType.Texture) + continue; + if ((propertyFlags & (ShaderPropertyFlags.PerRendererData | ShaderPropertyFlags.HideInInspector | ShaderPropertyFlags.NonModifiableTextureData)) != 0) + continue; + + var propertyName = obj.shader.GetPropertyName(i); + var propertyValue = obj.GetTexture(propertyName); + + if (propertyValue == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"Texture property '{propertyName}' is Null")); + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs.meta b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs.meta new file mode 100644 index 0000000..d975053 --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfeb4b29a9cd4ea79c1e9325b8122e17 +timeCreated: 1779627240 \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorObsolete.cs b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs new file mode 100644 index 0000000..0eb3c19 --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs @@ -0,0 +1,12 @@ +using System; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AttributeValidatorObsolete : IAttributeValidator + { + public ValidatorResult Validate(ObsoleteAttribute attribute, object value) + { + return ValidatorResult.Create(EValidatorSeverity.Error, "Obsolete"); + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorObsolete.cs.meta b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs.meta new file mode 100644 index 0000000..2a54b7b --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 982ac4e898cc2ca438e98a2f0034a8d3 \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs b/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs index 1ea0509..eaf89a1 100644 --- a/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs +++ b/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Reflection; using UnityEngine; using UnityEngine.Pool; @@ -28,7 +30,7 @@ namespace Module.ProjectValidator.Editor } else { - if (count > 1) + if (count > 1 && IsMultipleComponentsAllowed(type)) results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has duplicate '{type.Name}' ({count}) components")); type = t; @@ -36,8 +38,13 @@ namespace Module.ProjectValidator.Editor } } - if (count > 1) + if (count > 1 && IsMultipleComponentsAllowed(type)) results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has duplicate '{type.Name}' ({count}) components")); } + + private static bool IsMultipleComponentsAllowed(Type type) + { + return type.GetCustomAttribute(true) == null; + } } } \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorTransform.cs b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs new file mode 100644 index 0000000..58b6f92 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class GameObjectValidatorTransform : IGameObjectValidator + { + public void Validate(GameObject gameObject, List results) + { + var transform = gameObject.transform; + var lp = transform.localPosition; + var lr = transform.localRotation; + var ls = transform.localScale; + + if (IsInvalid(lp.x) || IsInvalid(lp.y) || IsInvalid(lp.z)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Local position '{lp}' is invalid")); + if (IsInvalid(lr.x) || IsInvalid(lr.y) || IsInvalid(lr.z) || IsInvalid(lr.w)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Local rotation '{lr}' is invalid")); + if (IsInvalid(ls.x) || IsInvalid(ls.y) || IsInvalid(ls.z)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Local scale '{ls}' is invalid")); + } + + private static bool IsInvalid(float value) + { + return float.IsNaN(value) || float.IsInfinity(value); + } + } +} \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorTransform.cs.meta b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs.meta new file mode 100644 index 0000000..89d7bda --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 727d5de59b004deb8c192337bcee132e +timeCreated: 1779628852 \ No newline at end of file diff --git a/Editor/Window/EditorProjectValidatorWindow.cs b/Editor/Window/EditorProjectValidatorWindow.cs index 4989d7d..1d01017 100644 --- a/Editor/Window/EditorProjectValidatorWindow.cs +++ b/Editor/Window/EditorProjectValidatorWindow.cs @@ -8,12 +8,18 @@ namespace Module.ProjectValidator.Editor { internal sealed class EditorProjectValidatorWindow : EditorWindow { + private VisualElement _groupWarnings; + private Label _labelWarnings; + + private VisualElement _groupErrors; + private Label _labelErrors; + private MultiColumnTreeView _treeView; private string _searchFilter; - + private readonly List> _list = new(); private readonly List> _filteredList = new(); - + public void CreateGUI() { var root = rootVisualElement; @@ -21,9 +27,19 @@ namespace Module.ProjectValidator.Editor root.styleSheets.Add(EditorAssetUtility.LoadFirstAsset("StyleSheetEditorProjectValidatorWindow")); root.Add(asset.Instantiate()); + _groupWarnings = root.Q("status-warnings"); + _labelWarnings = _groupWarnings.Q