using System; using System.Collections.Generic; using System.Linq; using RPGCore.Editor.CoreManager.Validators; using RPGCoreCommon.Helpers; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace RPGCore.Editor.CoreManager { internal static class ValidationSolver { private static readonly List Validators = TypeCache.GetTypesDerivedFrom() .Select(Activator.CreateInstance) .Cast() .ToList(); [InitializeOnLoadMethod] private static void AutomaticValidation_Initialize() => ObjectChangeEvents.changesPublished += AutomaticValidation; private static void AutomaticValidation(ref ObjectChangeEventStream stream) { var instanceIds = new List(); for (var i = 0; i < stream.length; i++) { if (stream.GetEventType(i) != ObjectChangeKind.ChangeGameObjectStructure) continue; stream.GetChangeGameObjectStructureEvent(0, out var ev); instanceIds.Add(ev.instanceId); } if (instanceIds.Count == 0) return; var validations = Check(instanceIds); FixAll(validations); // Can't finish because of exception or error validations .Where(v => v.result == false) .Select(v => "--- ERROR: " + v.resultMessage + "\n> " + v.contextPath + "\n> " + v.hierarchyPath + "\n> " + v.validator.GetType().Name + "\n> " + v.message) .ForEach(Debug.LogError); // No fix defined validations .Where(v => v.result == null) .Select(v => "--- WARNING: " + v.resultMessage + "\n> " + v.contextPath + "\n> " + v.hierarchyPath + "\n> " + v.validator.GetType().Name + "\n> " + v.message) .ForEach(Debug.LogWarning); } internal static List Check(List instanceIds) { var validations = new List(); foreach (var instanceId in instanceIds) { var obj = EditorUtility.EntityIdToObject(instanceId); if (obj is not GameObject go) continue; Check(go, validations, false); } return validations; } internal static List Check(string[] paths) { var validations = new List(); foreach (var guid in AssetDatabase.FindAssetGUIDs("t:Scene", paths)) { var scene = EditorSceneManager.OpenPreviewScene(AssetDatabase.GUIDToAssetPath(guid)); foreach (var go in scene.GetRootGameObjects()) { try { Check(go, validations, true); } catch (Exception) { // ignored } } EditorSceneManager.ClosePreviewScene(scene); } foreach (var guid in AssetDatabase.FindAssetGUIDs("t:Prefab", paths)) { var prefabPath = AssetDatabase.GUIDToAssetPath(guid); var prefab = PrefabUtility.LoadPrefabContents(prefabPath); try { Check(prefab, validations, true); } catch (Exception) { // ignored } PrefabUtility.UnloadPrefabContents(prefab); } return validations; } internal static void Check(GameObject go, List result, bool recursive) { // On scene and part of other prefab - don't validate that, it will be validated separately anyway if (PrefabUtility.IsPartOfPrefabInstance(go)) return; foreach (var validator in Validators) { var message = validator.Check(go); if (string.IsNullOrEmpty(message)) continue; result.Add(new Validation { validator = validator, message = message, contextPath = GetContextPath(go), hierarchyPath = GetHierarchyPath(go), }); } if (!recursive) return; for (var i = 0; i < go.transform.childCount; i++) Check(go.transform.GetChild(i).gameObject, result, true); } internal static void FixAll(List validations) { foreach (var validation in validations) { var contextType = AssetDatabase.GetMainAssetTypeAtPath(validation.contextPath); if (contextType == typeof(SceneAsset)) FixOnScene(validation); else if (contextType == typeof(GameObject)) FixOnPrefab(validation); else throw new Exception($"Given file '{validation.contextPath}' is not prefab nor scene!"); } } private static void FixOnScene(Validation validation) { var scene = Enumerable.Range(0, SceneManager.loadedSceneCount) .Select(SceneManager.GetSceneAt) .FirstOrDefault(scene => scene.path == validation.contextPath); var isActive = scene.IsValid(); if (!isActive) scene = EditorSceneManager.OpenPreviewScene(validation.contextPath); var go = FindGameObject(scene.GetRootGameObjects(), validation); FixGameObject(go, validation); if (isActive) EditorUtility.SetDirty(go); if (!isActive) EditorSceneManager.SaveScene(scene); if (!isActive) EditorSceneManager.ClosePreviewScene(scene); } private static void FixOnPrefab(Validation validation) { var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); var isActive = prefabStage && prefabStage.assetPath == validation.contextPath; var prefabRoot = isActive ? prefabStage.prefabContentsRoot : PrefabUtility.LoadPrefabContents(validation.contextPath); var go = FindGameObject(new[] { prefabRoot }, validation); FixGameObject(go, validation); if (isActive) EditorUtility.SetDirty(go); if (!isActive) PrefabUtility.SaveAsPrefabAsset(prefabRoot, validation.contextPath); if (!isActive) PrefabUtility.UnloadPrefabContents(prefabRoot); } private static GameObject FindGameObject(GameObject[] roots, Validation validation) { var hierarchyPathArray = validation.hierarchyPath.Split('/', 2); var root = roots.FirstOrDefault(go => go.name == hierarchyPathArray[0]); if (!root) throw new Exception($"Root '{hierarchyPathArray[0]}' not found in '{validation.contextPath}'!"); var go = hierarchyPathArray.Length > 1 ? root.transform.Find(hierarchyPathArray[1])?.gameObject : root; if (!go) throw new Exception($"GameObject '{validation.hierarchyPath}' not found in '{validation.contextPath}'!"); return go; } private static void FixGameObject(GameObject go, Validation validation) { try { validation.validator.Fix(go); validation.resultMessage = "Done."; validation.result = true; } catch (NotImplementedException) { validation.resultMessage = "No Auto-fix defined. Do it yourself."; } catch (Exception e) { validation.result = false; validation.resultMessage = e.Message; } } private static string GetContextPath(GameObject go) { // ON PREFAB STAGE - this one will be used only in realtime correcting, never in verify tab if (PrefabStageUtility.GetPrefabStage(go) is {} stage) return stage.assetPath; // ON SCENE if (go.scene.IsValid()) return go.scene.path; // PREFAB ASSET if (PrefabUtility.IsPartOfPrefabAsset(go)) return go.scene.path; throw new Exception("No valid context path found, sad pepe :("); } private static string GetHierarchyPath(GameObject go) { var path = new List(); while (go is not null) { path.Add(go.name); go = go.transform.parent?.gameObject; } return string.Join("/", path.AsEnumerable().Reverse()); } } }