Added initial version 0.1.0

This commit is contained in:
Anders Ejlersen 2026-05-17 22:12:04 +02:00
parent 6aa4cb8596
commit 416759c213
64 changed files with 2181 additions and 0 deletions

View file

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Module.ProjectValidator.Editor
{
internal static class EditorAssetUtility
{
public static T LoadFirstAsset<T>(string name) where T : Object
{
var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{typeof(T).Name} {name}");
return guids.Length != 0 ? AssetDatabase.LoadAssetByGUID<T>(guids[0]) : null;
}
public static T[] LoadAllAssets<T>() where T : Object
{
var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{typeof(T).Name}");
var list = new List<T>(guids.Length);
foreach (var guid in guids)
{
var asset = AssetDatabase.LoadAssetByGUID<T>(guid);
if (asset != null)
list.Add(asset);
}
return list.ToArray();
}
internal static GUID GetAssetGuid(Object obj)
{
var assetGuid = new GUID();
if (obj is GameObject gameObject)
{
if (gameObject.scene.isLoaded)
GUID.TryParse(AssetDatabase.AssetPathToGUID(gameObject.scene.path), out assetGuid);
else if (PrefabUtility.IsPartOfPrefabAsset(gameObject))
GUID.TryParse(AssetDatabase.AssetPathToGUID(gameObject.scene.path), out assetGuid);
}
else
{
GUID.TryParse(AssetDatabase.GetAssetPath(obj), out assetGuid);
}
return assetGuid;
}
internal static string GetAssetName(GUID assetGuid)
{
if (assetGuid.Empty())
return string.Empty;
var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
return Path.GetFileNameWithoutExtension(assetPath);
}
internal static GUID ObjectToAssetGuid(Object obj)
{
var assetPath = AssetDatabase.GetAssetPath(obj);
var strGuid = AssetDatabase.AssetPathToGUID(assetPath);
return GUID.TryParse(strGuid, out var guid) ? guid : new GUID();
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: afafd2a389cc46b2bc66029cb8d2cfea
timeCreated: 1778924949

View file

@ -0,0 +1,44 @@
using UnityEngine;
namespace Module.ProjectValidator.Editor
{
internal static class EditorIconUtility
{
private static bool _initialized;
private static GUIContent _warning;
private static GUIContent _error;
private static GUIContent _warningRedirect;
private static GUIContent _errorRedirect;
private static GUIStyle _style;
private static void Initialize()
{
_initialized = true;
_warning = new GUIContent(EditorAssetUtility.LoadFirstAsset<Texture>("editor_project_validator_warning"));
_error = new GUIContent(EditorAssetUtility.LoadFirstAsset<Texture>("editor_project_validator_error"));
_warningRedirect = new GUIContent(EditorAssetUtility.LoadFirstAsset<Texture>("editor_project_validator_warning_redirect"));
_errorRedirect = new GUIContent(EditorAssetUtility.LoadFirstAsset<Texture>("editor_project_validator_error_redirect"));
_style = new GUIStyle();
}
public static GUIContent GetIcon(EValidatorSeverity severity, bool isRedirect)
{
if (!_initialized)
Initialize();
if (isRedirect)
return severity == EValidatorSeverity.Warning ? _warningRedirect : _errorRedirect;
return severity == EValidatorSeverity.Warning ? _warning : _error;
}
public static void Draw(Rect rect, EValidatorSeverity severity, bool isRedirect)
{
if (Event.current.type != EventType.Repaint)
return;
var content = GetIcon(severity, isRedirect);
_style.Draw(rect, content, -1);
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d246451b954e46769686738d623b833b
timeCreated: 1779021007

View file

@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.SceneManagement;
namespace Module.ProjectValidator.Editor
{
public static class ProjectValidatorUtility
{
[MenuItem("Window/Analysis/Project Validator")]
public static void OpenWindow()
{
var window = InternalOpenWindow();
window.Show();
window.Rebuild();
}
public static void ClearWindow()
{
var windows = Resources.FindObjectsOfTypeAll<EditorProjectValidatorWindow>();
for (var i = 0; i < windows.Length; i++)
{
windows[i].Clear();
}
RefreshUnityWindows();
}
private static EditorProjectValidatorWindow InternalOpenWindow()
{
var window = EditorWindow.GetWindow<EditorProjectValidatorWindow>();
window.titleContent = new GUIContent("Project Validator");
return window;
}
internal static string GetAttributeShortName(Attribute attribute)
{
var str = attribute.GetType().Name;
var index = str.IndexOf("Attribute", StringComparison.Ordinal);
if (index != -1)
str = str[..index];
str = ObjectNames.NicifyVariableName(str);
return str;
}
internal static void AppendToScenePath(GameObject gameObject, ref string scenePath)
{
scenePath = string.IsNullOrEmpty(scenePath) ? gameObject.name : $"{scenePath}/{gameObject.name}";
}
internal static string ApplyRichTextToScenePath(string scenePath)
{
return scenePath.Replace("/", "<color=#00ff00><b>/</b></color>");
}
public static void AppendToFieldPath(FieldInfo fieldInfo, ref string fieldPath)
{
fieldPath = string.IsNullOrEmpty(fieldPath) ? fieldInfo.Name : $"{fieldPath}.{fieldInfo.Name}";
}
public static void AppendToFieldPath(int index, ref string fieldPath)
{
fieldPath += $"[{index}]";
}
public static string ApplyRichTextToFieldPath(string fieldPath)
{
var str = fieldPath.Replace(".", "<color=#00ff00><b>.</b></color>");
str = str.Replace("[", "<color=#00ff00><b>[</b></color>");
str = str.Replace("]", "<color=#00ff00><b>]</b></color>");
return str;
}
internal static void PingObject(Report.Entry entry)
{
if (entry.AssetGuid.Empty())
return;
var assetPath = AssetDatabase.GUIDToAssetPath(entry.AssetGuid);
var asset = AssetDatabase.LoadMainAssetAtPath(assetPath);
if (asset == null)
return;
if (asset is SceneAsset)
{
var scene = SceneManager.GetSceneByPath(assetPath);
if (scene.isLoaded && TryFindSceneObjectByPath(scene, entry.ScenePath, out var gameObject))
EditorGUIUtility.PingObject(gameObject);
else
EditorGUIUtility.PingObject(asset);
}
else
{
EditorGUIUtility.PingObject(asset);
}
}
private static bool TryFindSceneObjectByPath(Scene scene, string scenePath, out GameObject gameObject)
{
using var _ = ListPool<GameObject>.Get(out var rootObjects);
scene.GetRootGameObjects(rootObjects);
var index = scenePath.IndexOf('/');
var rootName = index != -1 ? scenePath[..index] : scenePath;
var childPath = index != -1 ? scenePath[(index + 1)..] : string.Empty;
for (var i = 0; i < rootObjects.Count; i++)
{
var rootObject = rootObjects[i];
if (rootObject.name != rootName)
continue;
if (string.IsNullOrEmpty(childPath))
{
gameObject = rootObject;
return true;
}
var child = rootObject.transform.Find(childPath);
if (child == null)
continue;
gameObject = child.gameObject;
return true;
}
gameObject = null;
return false;
}
internal static void RebuildAssetMapping(Dictionary<GUID, Report.MappingEntry> dictMapping)
{
using var _ = DictionaryPool<GUID, Report.MappingEntry>.Get(out var newMappings);
foreach (var pair in dictMapping)
{
var severity = pair.Value.Severity;
var assetPath = AssetDatabase.GUIDToAssetPath(pair.Key);
var folderPath = Path.GetDirectoryName(assetPath);
while (!string.IsNullOrEmpty(folderPath))
{
var strGuid = AssetDatabase.AssetPathToGUID(folderPath);
if (GUID.TryParse(strGuid, out var assetGuid))
{
if (dictMapping.TryGetValue(assetGuid, out var parentMapping))
{
if (severity < parentMapping.Severity)
severity = parentMapping.Severity;
}
else if (newMappings.TryGetValue(assetGuid, out var currentMapping))
{
if (currentMapping.Severity < severity)
newMappings[assetGuid] = new Report.MappingEntry(severity, true);
}
else
{
newMappings.Add(assetGuid, new Report.MappingEntry(severity, true));
}
}
folderPath = Path.GetDirectoryName(folderPath);
}
}
foreach (var pair in newMappings)
{
dictMapping.Add(pair.Key, pair.Value);
}
}
internal static void RebuildSceneInstanceMapping(Report report, Dictionary<int, Report.MappingEntry> dictMapping)
{
dictMapping.Clear();
using var _ = ListPool<GameObject>.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 scenePath = string.Empty;
RebuildSceneInstanceMapping(report, dictMapping, rootObject, assetGuid, scenePath);
}
}
RebuildForAllParents(dictMapping);
}
private static void RebuildSceneInstanceMapping(Report report, Dictionary<int, Report.MappingEntry> dictMapping, GameObject gameObject, GUID assetGuid, string scenePath)
{
var transform = gameObject.transform;
AppendToScenePath(gameObject, ref scenePath);
if (report.TryGetSeverityFor(assetGuid, scenePath, out var mapping))
dictMapping.Add(gameObject.GetInstanceID(), new Report.MappingEntry(mapping.Severity, false));
for (var i = 0; i < transform.childCount; i++)
{
RebuildSceneInstanceMapping(report, dictMapping, transform.GetChild(i).gameObject, assetGuid, scenePath);
}
}
private static void RebuildForAllParents(Dictionary<int, Report.MappingEntry> dictMapping)
{
using var _ = DictionaryPool<int, Report.MappingEntry>.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 instanceId = gameObject.GetInstanceID();
if (dictMapping.TryGetValue(instanceId, out var parentMapping))
{
if (severity < parentMapping.Severity)
severity = parentMapping.Severity;
}
else if (newMappings.TryGetValue(instanceId, out var currentMapping))
{
if (currentMapping.Severity < severity)
newMappings[instanceId] = new Report.MappingEntry(severity, true);
}
else
{
newMappings.Add(instanceId, new Report.MappingEntry(severity, true));
}
transform = transform.parent;
}
}
foreach (var pair in newMappings)
{
dictMapping.Add(pair.Key, pair.Value);
}
}
internal static void RefreshUnityWindows()
{
EditorApplication.RepaintHierarchyWindow();
EditorApplication.RepaintProjectWindow();
}
internal static bool IsValidForRun()
{
return !EditorApplication.isPlaying && !EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isCompiling;
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7775e42f8dfc437db085465caa4eefd9
timeCreated: 1778924236