diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..f019fd0 --- /dev/null +++ b/.vsconfig @@ -0,0 +1,6 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Workload.ManagedGame" + ] +} diff --git a/Assets/Plugins.meta b/Assets/Plugins.meta new file mode 100644 index 0000000..8cfd9f1 --- /dev/null +++ b/Assets/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 27ff2c6a21a006c458bd022e98146d74 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector.meta b/Assets/Plugins/AssetUsageDetector.meta new file mode 100644 index 0000000..07f3044 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: c1764d69117881843b761dc14ca276d4 +folderAsset: yes +timeCreated: 1561225368 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor.meta b/Assets/Plugins/AssetUsageDetector/Editor.meta new file mode 100644 index 0000000..3634eb5 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 19f677a9eb83d3942af6d4c5fa8dbeee +folderAsset: yes +timeCreated: 1520032274 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.Editor.asmdef b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.Editor.asmdef new file mode 100644 index 0000000..f02732f --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.Editor.asmdef @@ -0,0 +1,29 @@ +{ + "name": "AssetUsageDetector.Editor", + "rootNamespace": "", + "references": [ + "Unity.Addressables" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.unity.addressables", + "expression": "0.0.0", + "define": "ASSET_USAGE_ADDRESSABLES" + }, + { + "name": "com.unity.visualeffectgraph", + "expression": "0.0.0", + "define": "ASSET_USAGE_VFX_GRAPH" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.Editor.asmdef.meta b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.Editor.asmdef.meta new file mode 100644 index 0000000..a29079c --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8579ab42c9ab63d4bac5fb07bd390b46 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs new file mode 100644 index 0000000..b1a0a22 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs @@ -0,0 +1,1398 @@ +// Asset Usage Detector - by Suleyman Yasir KULA (yasirkula@gmail.com) + +using UnityEngine; +using UnityEditor; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using System.Collections.Generic; +using System.Reflection; +using System; +using System.IO; +using System.Text; +using Object = UnityEngine.Object; +#if UNITY_2018_3_OR_NEWER && !UNITY_2021_2_OR_NEWER +using PrefabStage = UnityEditor.Experimental.SceneManagement.PrefabStage; +using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility; +#endif + +namespace AssetUsageDetectorNamespace +{ + [Flags] + public enum SceneSearchMode { None = 0, OpenScenes = 1, ScenesInBuildSettingsAll = 2, ScenesInBuildSettingsTickedOnly = 4, AllScenes = 8 }; + + public partial class AssetUsageDetector + { + #region Helper Classes + [Serializable] + public class Parameters + { + public Object[] objectsToSearch = null; + + public SceneSearchMode searchInScenes = SceneSearchMode.AllScenes; + public Object[] searchInScenesSubset = null; + public Object[] excludedScenesFromSearch = null; + public bool searchInSceneLightingSettings = true; + public bool searchInAssetsFolder = true; + public Object[] searchInAssetsSubset = null; + public Object[] excludedAssetsFromSearch = null; + public bool dontSearchInSourceAssets = true; + public bool searchInProjectSettings = true; + + public int searchDepthLimit = 4; + public BindingFlags fieldModifiers = BindingFlags.Public | BindingFlags.NonPublic; + public BindingFlags propertyModifiers = BindingFlags.Public | BindingFlags.NonPublic; + public bool searchNonSerializableVariables = true; + + public bool searchUnusedMaterialProperties = true; + + public SearchRefactoring searchRefactoring = null; + + public bool lazySceneSearch = true; +#if ASSET_USAGE_ADDRESSABLES + public bool addressablesSupport = false; +#endif + public bool calculateUnusedObjects = false; + public bool hideDuplicateRows = true; + public bool hideReduntantPrefabVariantLinks = true; + public bool noAssetDatabaseChanges = false; + public bool showDetailedProgressBar = true; + } + #endregion + + private Parameters searchParameters; + + // A set that contains the searched scene object(s), asset(s) and their sub-assets (if any) + private readonly HashSet objectsToSearchSet = new HashSet(); + // Scenes of scene object(s) in objectsToSearchSet + private readonly HashSet sceneObjectsToSearchScenesSet = new HashSet(); + // Project asset(s) in objectsToSearchSet + private readonly HashSet assetsToSearchSet = new HashSet(); + // assetsToSearchSet's path(s) + private readonly HashSet assetsToSearchPathsSet = new HashSet(); + // The root prefab objects in assetsToSearchSet that will be used to search for prefab references + private readonly List assetsToSearchRootPrefabs = new List( 4 ); + // Path(s) of the assets that should be excluded from the search + private readonly HashSet excludedAssetsPathsSet = new HashSet(); + // Extension(s) of assets that will always be searched in detail + private readonly HashSet alwaysSearchedExtensionsSet = new HashSet(); + + // Results for the currently searched scene + private SearchResultGroup currentSearchResultGroup; + + // An optimization to search an object only once (key is a hash of the searched object) + private readonly Dictionary searchedObjects = new Dictionary( 4096 ); + private readonly Dictionary searchedUnityObjects = new Dictionary( 32768 ); // Unity objects use their instanceIDs as key which is more performant + + // Stack of SearchObject function parameters to avoid infinite loops (which happens when same object is passed as parameter to function) + private readonly List callStack = new List( 64 ); + + private Object currentSearchedObject; + private int currentDepth; + + private bool searchingSourceAssets; + private bool isInPlayMode; + +#if UNITY_2018_3_OR_NEWER + private PrefabStage openPrefabStage; + private GameObject openPrefabStagePrefabAsset; +#if UNITY_2020_1_OR_NEWER + private GameObject openPrefabStageContextObject; +#endif +#endif + + private int searchedObjectsCount; // Number of searched objects + private double searchStartTime; + + private readonly List nodesPool = new List( 32 ); + + // Search for references! + public SearchResult Run( Parameters searchParameters ) + { + if( searchParameters == null ) + { + Debug.LogError( "'searchParameters' mustn't be null!" ); + return new SearchResult( false, null, null, null, this, searchParameters ); + } + + if( searchParameters.objectsToSearch == null ) + { + Debug.LogError( "'objectsToSearch' list (\"SEARCHED OBJECTS\") is empty!" ); + return new SearchResult( false, null, null, null, this, searchParameters ); + } + +#if UNITY_2018_3_OR_NEWER + openPrefabStagePrefabAsset = null; + string openPrefabStageAssetPath = null; + openPrefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if( openPrefabStage != null ) + { + if( !openPrefabStage.stageHandle.IsValid() ) + openPrefabStage = null; + else + { + if( openPrefabStage.scene.isDirty ) + { + // Don't start the search if a prefab stage is currently open and dirty (not saved) + Debug.LogError( "Save open prefab first!" ); + return new SearchResult( false, null, null, null, this, searchParameters ); + } + +#if UNITY_2020_1_OR_NEWER + string prefabAssetPath = openPrefabStage.assetPath; +#else + string prefabAssetPath = openPrefabStage.prefabAssetPath; +#endif + openPrefabStagePrefabAsset = AssetDatabase.LoadAssetAtPath( prefabAssetPath ); + openPrefabStageAssetPath = prefabAssetPath; + +#if UNITY_2020_1_OR_NEWER + openPrefabStageContextObject = openPrefabStage.openedFromInstanceRoot; +#endif + } + } +#endif + + List searchResult = null; + isInPlayMode = EditorApplication.isPlaying; + + if( !isInPlayMode && !Utilities.AreScenesSaved() && !EditorUtility.DisplayDialog( "Asset Usage Detector", "Some scene(s) aren't saved. This may result in incorrect search results in those scene(s). Proceed?", "Yes", "Cancel" ) ) + { + Debug.LogError( "Save open scene(s) first!" ); + return new SearchResult( false, null, null, null, this, searchParameters ); + } + + // Get the scenes that are open right now + SceneSetup[] initialSceneSetup = !isInPlayMode ? EditorSceneManager.GetSceneManagerSetup() : null; + Scene activeScene = EditorSceneManager.GetActiveScene(); + + // Make sure that the AssetDatabase is up-to-date + AssetDatabase.SaveAssets(); + + try + { + this.searchParameters = searchParameters; + + // Initialize commonly used variables + searchResult = new List(); // Overall search results + + currentSearchedObject = null; + currentDepth = 0; + searchedObjectsCount = 0; + searchStartTime = EditorApplication.timeSinceStartup; + + searchedObjects.Clear(); + searchedUnityObjects.Clear(); + animationClipUniqueBindings.Clear(); + callStack.Clear(); + objectsToSearchSet.Clear(); + sceneObjectsToSearchScenesSet.Clear(); + assetsToSearchSet.Clear(); + assetsToSearchPathsSet.Clear(); + assetsToSearchRootPrefabs.Clear(); + excludedAssetsPathsSet.Clear(); + alwaysSearchedExtensionsSet.Clear(); + shaderIncludesToSearchSet.Clear(); +#if UNITY_2017_3_OR_NEWER + assemblyDefinitionFilesToSearch.Clear(); +#endif + + if( assetDependencyCache == null ) + { + LoadCache(); + searchStartTime = EditorApplication.timeSinceStartup; + } + else if( !searchParameters.noAssetDatabaseChanges ) + { + foreach( var cacheEntry in assetDependencyCache.Values ) + cacheEntry.verified = false; + } + + foreach( var cacheEntry in assetDependencyCache.Values ) + cacheEntry.searchResult = CacheEntry.Result.Unknown; + + lastRefreshedCacheEntry = null; + + // Store the searched objects(s) in HashSets + HashSet folderContentsSet = new HashSet(); + foreach( Object obj in searchParameters.objectsToSearch ) + { + if( obj == null || obj.Equals( null ) ) + continue; + + if( obj.IsFolder() ) + folderContentsSet.UnionWith( Utilities.EnumerateFolderContents( obj ) ); + else + AddSearchedObjectToFilteredSets( obj, true ); + } + + foreach( string filePath in folderContentsSet ) + { + // Skip scene assets + if( filePath.EndsWithFast( ".unity" ) ) + continue; + + Object[] assets = AssetDatabase.LoadAllAssetsAtPath( filePath ); + if( assets == null || assets.Length == 0 ) + continue; + + for( int i = 0; i < assets.Length; i++ ) + AddSearchedObjectToFilteredSets( assets[i], true ); + } + + // Find Project Settings to search for references. Don't search Project Settings if searched object(s) are all scene objects + // as Project Settings can't hold references to scene objects + string[] projectSettingsToSearch = new string[0]; + if( searchParameters.searchInProjectSettings && assetsToSearchSet.Count > 0 ) + { + string[] projectSettingsAssets = Directory.GetFiles( "ProjectSettings" ); + projectSettingsToSearch = new string[projectSettingsAssets.Length]; + for( int i = 0; i < projectSettingsAssets.Length; i++ ) + projectSettingsToSearch[i] = "ProjectSettings/" + Path.GetFileName( projectSettingsAssets[i] ); + + // AssetDatabase.GetDependencies doesn't work with Project Settings assets. By adding these assets to assetsToSearchPathsSet, + // we make sure that AssetHasAnyReference returns true for these assets and they don't get excluded from the search + assetsToSearchPathsSet.UnionWith( projectSettingsToSearch ); + } + + // Find the scenes to search for references + HashSet scenesToSearch = new HashSet(); + if( searchParameters.searchInScenesSubset != null && searchParameters.searchInScenesSubset.Length > 0 ) + { + foreach( Object obj in searchParameters.searchInScenesSubset ) + { + if( obj == null || obj.Equals( null ) ) + continue; + + if( !obj.IsAsset() ) + continue; + + if( obj.IsFolder() ) + { + string[] folderContents = AssetDatabase.FindAssets( "t:SceneAsset", new string[] { AssetDatabase.GetAssetPath( obj ) } ); + if( folderContents == null ) + continue; + + for( int i = 0; i < folderContents.Length; i++ ) + scenesToSearch.Add( AssetDatabase.GUIDToAssetPath( folderContents[i] ) ); + } + else if( obj is SceneAsset ) + scenesToSearch.Add( AssetDatabase.GetAssetPath( obj ) ); + } + } + else if( ( searchParameters.searchInScenes & SceneSearchMode.AllScenes ) == SceneSearchMode.AllScenes ) + { + // Get all scenes from the Assets folder + string[] sceneGuids = AssetDatabase.FindAssets( "t:SceneAsset" ); + for( int i = 0; i < sceneGuids.Length; i++ ) + scenesToSearch.Add( AssetDatabase.GUIDToAssetPath( sceneGuids[i] ) ); + } + else + { + if( ( searchParameters.searchInScenes & SceneSearchMode.OpenScenes ) == SceneSearchMode.OpenScenes ) + { + // Get all open (and loaded) scenes + for( int i = 0; i < SceneManager.sceneCount; i++ ) + { + Scene scene = SceneManager.GetSceneAt( i ); + if( scene.IsValid() && scene.isLoaded ) + scenesToSearch.Add( scene.path ); + } + } + + bool searchInScenesInBuildTickedAll = ( searchParameters.searchInScenes & SceneSearchMode.ScenesInBuildSettingsAll ) == SceneSearchMode.ScenesInBuildSettingsAll; + if( searchInScenesInBuildTickedAll || ( searchParameters.searchInScenes & SceneSearchMode.ScenesInBuildSettingsTickedOnly ) == SceneSearchMode.ScenesInBuildSettingsTickedOnly ) + { + // Get all scenes in build settings + EditorBuildSettingsScene[] scenesTemp = EditorBuildSettings.scenes; + for( int i = 0; i < scenesTemp.Length; i++ ) + { + if( ( searchInScenesInBuildTickedAll || scenesTemp[i].enabled ) ) + scenesToSearch.Add( scenesTemp[i].path ); + } + } + } + + // In Play mode, only open scenes can be searched + if( isInPlayMode ) + { + HashSet openScenes = new HashSet(); + for( int i = 0; i < SceneManager.sceneCount; i++ ) + { + Scene scene = SceneManager.GetSceneAt( i ); + if( scene.IsValid() && scene.isLoaded ) + openScenes.Add( scene.path ); + } + + List skippedScenes = new List( scenesToSearch.Count ); + scenesToSearch.RemoveWhere( ( path ) => + { + if( !openScenes.Contains( path ) ) + { + skippedScenes.Add( path ); + return true; + } + + return false; + } ); + + if( skippedScenes.Count > 0 ) + { + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + sb.Append( "Can't search unloaded scenes while in play mode, skipped " ).Append( skippedScenes.Count ).AppendLine( " scene(s):" ); + for( int i = 0; i < skippedScenes.Count; i++ ) + sb.Append( "- " ).AppendLine( skippedScenes[i] ); + + Debug.Log( sb.ToString() ); + } + } + + // Initialize data used by search functions + InitializeSearchFunctionsData( searchParameters ); + + // Initialize the nodes of searched asset(s) + foreach( Object obj in objectsToSearchSet ) + searchedUnityObjects.Add( obj.GetInstanceID(), PopReferenceNode( obj ) ); + + // Progressbar values + int searchProgress = 0; + int searchTotalProgress = scenesToSearch.Count; + if( isInPlayMode && searchParameters.searchInScenes != SceneSearchMode.None ) + searchTotalProgress++; // DontDestroyOnLoad scene + + if( searchParameters.showDetailedProgressBar ) + searchTotalProgress += projectSettingsToSearch.Length; + + // Don't search assets if searched object(s) are all scene objects as assets can't hold references to scene objects + if( searchParameters.searchInAssetsFolder && assetsToSearchSet.Count > 0 ) + { + currentSearchResultGroup = new SearchResultGroup( "Project Window (Assets)", SearchResultGroup.GroupType.Assets ); + + // Get the paths of all assets that are to be searched + IEnumerable assetPaths; + if( searchParameters.searchInAssetsSubset == null || searchParameters.searchInAssetsSubset.Length == 0 ) + { + string[] allAssetPaths = AssetDatabase.GetAllAssetPaths(); + assetPaths = allAssetPaths; + + if( searchParameters.showDetailedProgressBar ) + searchTotalProgress += allAssetPaths.Length; + } + else + { + folderContentsSet.Clear(); + + foreach( Object obj in searchParameters.searchInAssetsSubset ) + { + if( obj == null || obj.Equals( null ) ) + continue; + + if( !obj.IsAsset() ) + continue; + + if( obj.IsFolder() ) + folderContentsSet.UnionWith( Utilities.EnumerateFolderContents( obj ) ); + else + folderContentsSet.Add( AssetDatabase.GetAssetPath( obj ) ); + } + + assetPaths = folderContentsSet; + + if( searchParameters.showDetailedProgressBar ) + searchTotalProgress += folderContentsSet.Count; + } + + // Calculate the path(s) of the assets that won't be searched for references + if( searchParameters.excludedAssetsFromSearch != null ) + { + foreach( Object obj in searchParameters.excludedAssetsFromSearch ) + { + if( obj == null || obj.Equals( null ) ) + continue; + + if( !obj.IsAsset() ) + continue; + + if( obj.IsFolder() ) + excludedAssetsPathsSet.UnionWith( Utilities.EnumerateFolderContents( obj ) ); + else + excludedAssetsPathsSet.Add( AssetDatabase.GetAssetPath( obj ) ); + } + } + + if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching assets", 0f ) ) + throw new Exception( "Search aborted" ); + + foreach( string path in assetPaths ) + { + if( searchParameters.showDetailedProgressBar && ++searchProgress % 30 == 1 && EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching assets", (float) searchProgress / searchTotalProgress ) ) + throw new Exception( "Search aborted" ); + + if( excludedAssetsPathsSet.Contains( path ) ) + continue; + + // If asset resides inside the Assets directory and is not a scene asset + if( path.StartsWithFast( "Assets/" ) && !path.EndsWithFast( ".unity" ) ) + { + if( !AssetHasAnyReference( path ) ) + continue; + + Object[] assets = AssetDatabase.LoadAllAssetsAtPath( path ); + if( assets == null || assets.Length == 0 ) + continue; + + for( int i = 0; i < assets.Length; i++ ) + { + // Components are already searched while searching the GameObject + if( assets[i] is Component ) + continue; + + BeginSearchObject( assets[i] ); + } + } + } + + // If a reference is found in the Project view, save the results + if( currentSearchResultGroup.NumberOfReferences > 0 ) + searchResult.Add( currentSearchResultGroup ); + } + + // Search all assets inside the ProjectSettings folder + if( projectSettingsToSearch.Length > 0 ) + { + currentSearchResultGroup = new SearchResultGroup( "Project Settings", SearchResultGroup.GroupType.ProjectSettings ); + + if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching Project Settings", (float) searchProgress / searchTotalProgress ) ) + throw new Exception( "Search aborted" ); + + for( int i = 0; i < projectSettingsToSearch.Length; i++ ) + { + if( searchParameters.showDetailedProgressBar && ++searchProgress % 30 == 1 && EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching Project Settings", (float) searchProgress / searchTotalProgress ) ) + throw new Exception( "Search aborted" ); + + Object[] assets = AssetDatabase.LoadAllAssetsAtPath( projectSettingsToSearch[i] ); + if( assets != null && assets.Length > 0 ) + { + for( int j = 0; j < assets.Length; j++ ) + BeginSearchObject( assets[j] ); + } + } + + if( currentSearchResultGroup.NumberOfReferences > 0 ) + searchResult.Add( currentSearchResultGroup ); + } + + // Search non-serializable variables for references while searching a scene in play mode + if( isInPlayMode ) + searchSerializableVariablesOnly = false; + + if( scenesToSearch.Count > 0 ) + { + // Calculate the path(s) of the scenes that won't be searched for references + HashSet excludedScenesPathsSet = new HashSet(); + if( searchParameters.excludedScenesFromSearch != null ) + { + foreach( Object obj in searchParameters.excludedScenesFromSearch ) + { + if( obj == null || obj.Equals( null ) ) + continue; + + if( !obj.IsAsset() ) + continue; + + if( obj.IsFolder() ) + { + string[] folderContents = AssetDatabase.FindAssets( "t:SceneAsset", new string[] { AssetDatabase.GetAssetPath( obj ) } ); + if( folderContents == null ) + continue; + + for( int i = 0; i < folderContents.Length; i++ ) + excludedScenesPathsSet.Add( AssetDatabase.GUIDToAssetPath( folderContents[i] ) ); + } + else if( obj is SceneAsset ) + excludedScenesPathsSet.Add( AssetDatabase.GetAssetPath( obj ) ); + } + } + + // Search scenes for references + foreach( string scenePath in scenesToSearch ) + { + if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching scene: " + scenePath, (float) ++searchProgress / searchTotalProgress ) ) + throw new Exception( "Search aborted" ); + + if( string.IsNullOrEmpty( scenePath ) ) + continue; + + if( excludedScenesPathsSet.Contains( scenePath ) ) + continue; + +#if UNITY_2019_2_OR_NEWER + // Skip scenes in read-only packages (Issue #36) + // Credit: https://forum.unity.com/threads/check-if-asset-inside-package-is-readonly.900902/#post-5990822 + if( !scenePath.StartsWithFast( "Assets/" ) ) + { + var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath( scenePath ); + if( packageInfo != null && packageInfo.source != UnityEditor.PackageManager.PackageSource.Embedded && packageInfo.source != UnityEditor.PackageManager.PackageSource.Local ) + continue; + } +#endif + + SearchScene( scenePath, searchResult, searchParameters, initialSceneSetup ); + } + } + + // Search through all the GameObjects under the DontDestroyOnLoad scene (if exists) + if( isInPlayMode && searchParameters.searchInScenes != SceneSearchMode.None ) + { + if( EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Searching scene: DontDestroyOnLoad", 1f ) ) + throw new Exception( "Search aborted" ); + + currentSearchResultGroup = new SearchResultGroup( "DontDestroyOnLoad", SearchResultGroup.GroupType.DontDestroyOnLoad ); + + GameObject[] rootGameObjects = GetDontDestroyOnLoadObjects(); + for( int i = 0; i < rootGameObjects.Length; i++ ) + SearchGameObjectRecursively( rootGameObjects[i] ); + + if( currentSearchResultGroup.NumberOfReferences > 0 ) + searchResult.Add( currentSearchResultGroup ); + } + + // Searching source assets last prevents some references from being excluded due to callStack.ContainsFast + if( !searchParameters.dontSearchInSourceAssets ) + { + searchingSourceAssets = true; + + foreach( Object obj in objectsToSearchSet ) + { + currentSearchedObject = obj; + SearchObject( obj ); + } + + searchingSourceAssets = false; + } + + EditorUtility.DisplayProgressBar( "Please wait...", "Post-processing search results", 1f ); + + InitializeSearchResultNodes( searchResult ); + + HashSet usedObjects = null; + if( searchResult.Count > 0 && searchParameters.calculateUnusedObjects ) + CalculateUnusedObjects( searchResult, out usedObjects ); + + // Log some c00l stuff to console + Debug.Log( "Searched " + searchedObjectsCount + " objects in " + ( EditorApplication.timeSinceStartup - searchStartTime ).ToString( "F2" ) + " seconds" ); + + return new SearchResult( true, searchResult, usedObjects, initialSceneSetup, this, searchParameters ); + } + catch( Exception e ) + { + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + sb.EnsureCapacity( objectsToSearchSet.Count * 50 + callStack.Count * 50 + 500 ); + + sb.AppendLine( "AssetUsageDetector Error: The following Exception is thrown during the search. Details:" ); + + Object latestUnityObjectInCallStack = AppendCallStackToStringBuilder( sb ); + + sb.AppendLine( "Searching references of: " ); + foreach( Object obj in objectsToSearchSet ) + { + if( obj ) + sb.Append( obj.name ).Append( " (" ).Append( obj.GetType() ).AppendLine( ")" ); + } + + Debug.LogError( sb.ToString(), latestUnityObjectInCallStack ); + Debug.LogException( e, latestUnityObjectInCallStack ); + + try + { + InitializeSearchResultNodes( searchResult ); + } + catch + { } + + return new SearchResult( false, searchResult, null, initialSceneSetup, this, searchParameters ); + } + finally + { + currentSearchResultGroup = null; + currentSearchedObject = null; + + EditorUtility.ClearProgressBar(); + + // If the active scene was changed during search, reset it + if( EditorSceneManager.GetActiveScene() != activeScene ) + EditorSceneManager.SetActiveScene( activeScene ); + +#if UNITY_2018_3_OR_NEWER + // If a prefab stage was open when the search was triggered, try reopening the prefab stage after the search is completed + if( !string.IsNullOrEmpty( openPrefabStageAssetPath ) ) + { +#if UNITY_2020_1_OR_NEWER + bool shouldOpenPrefabStageWithoutContext = true; + if( openPrefabStageContextObject != null && !openPrefabStageContextObject.Equals( null ) ) + { + try + { + // Try to access this method: https://github.com/Unity-Technologies/UnityCsReference/blob/73925b1711847c067e607ec8371f8e9ffe7ab65d/Editor/Mono/SceneManagement/StageManager/PrefabStage/PrefabStageUtility.cs#L61-L65 + MethodInfo prefabStageOpenerWithContext = typeof( PrefabStageUtility ).GetMethod( "OpenPrefab", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new Type[2] { typeof( string ), typeof( GameObject ) }, null ); + if( prefabStageOpenerWithContext != null ) + { + prefabStageOpenerWithContext.Invoke( null, new object[2] { openPrefabStageAssetPath, openPrefabStageContextObject } ); + shouldOpenPrefabStageWithoutContext = false; + } + } + catch { } + } + + if( shouldOpenPrefabStageWithoutContext ) +#endif + { + AssetDatabase.OpenAsset( AssetDatabase.LoadAssetAtPath( openPrefabStageAssetPath ) ); + } + } +#endif + } + } + + private void InitializeSearchResultNodes( List searchResult ) + { + for( int i = 0; i < searchResult.Count; i++ ) + { + searchResult[i].InitializeNodes( objectsToSearchSet ); + + // Remove empty search result groups + if( !searchResult[i].PendingSearch && searchResult[i].NumberOfReferences == 0 ) + searchResult.RemoveAt( i-- ); + } + } + + private void CalculateUnusedObjects( List searchResult, out HashSet usedObjectsSet ) + { + currentSearchResultGroup = new SearchResultGroup( "Unused Objects", SearchResultGroup.GroupType.UnusedObjects, false, false ); + + usedObjectsSet = new HashSet(); + HashSet usedObjectPathsSet = new HashSet(); // For assets: stores the filepaths, For scene objects: stores the topmost GameObject's instanceID + foreach( SearchResultGroup searchResultGroup in searchResult ) + { + for( int j = 0; j < searchResultGroup.NumberOfReferences; j++ ) + { + Object obj = searchResultGroup[j].UnityObject; + if( obj is Component ) + obj = ( (Component) obj ).gameObject; + + if( usedObjectsSet.Add( obj ) ) + { + string assetPath = AssetDatabase.GetAssetPath( obj ); + if( !string.IsNullOrEmpty( assetPath ) ) + usedObjectPathsSet.Add( assetPath ); + else + { + for( Transform parent = ( (GameObject) obj ).transform.parent; parent != null; parent = parent.parent ) + usedObjectPathsSet.Add( parent.gameObject.GetInstanceID().ToString() ); + } + } + } + } + + Dictionary unusedMainObjectNodes = new Dictionary( objectsToSearchSet.Count - usedObjectsSet.Count ); + Dictionary> unusedSubObjectNodes = new Dictionary>( objectsToSearchSet.Count - usedObjectsSet.Count ); + foreach( Object obj in objectsToSearchSet ) + { + // Omit components, their GameObjects are already included in search + if( obj is Component ) + continue; + + // Omit assets that are invisible in Hierarchy/Inspector + if( ( obj.hideFlags & ( HideFlags.HideInInspector | HideFlags.HideInHierarchy ) ) != HideFlags.None ) + continue; + + if( usedObjectsSet.Contains( obj ) ) + continue; + + string assetPath = AssetDatabase.GetAssetPath( obj ); + + // Omit unused sub-assets whose parent assets are used (configurable via Settings) + if( AssetUsageDetectorSettings.MarkUsedAssetsSubAssetsAsUsed && AssetDatabase.IsSubAsset( obj ) && usedObjectsSet.Contains( AssetDatabase.LoadMainAssetAtPath( assetPath ) ) ) + continue; + + // Omit meshes of an imported model asset + if( obj is Mesh && !string.IsNullOrEmpty( assetPath ) && AssetDatabase.GetMainAssetTypeAtPath( assetPath ) == typeof( GameObject ) && objectsToSearchSet.Contains( AssetDatabase.LoadMainAssetAtPath( assetPath ) ) ) + continue; + + // Omit MonoScripts whose types can't be determined + if( obj is MonoScript && ( (MonoScript) obj ).GetClass() == null ) + continue; + + GameObject searchedTopmostGameObject = null; + if( obj is GameObject ) + { + if( string.IsNullOrEmpty( assetPath ) ) + { + for( Transform parent = ( (GameObject) obj ).transform.parent; parent != null; parent = parent.parent ) + { + if( objectsToSearchSet.Contains( parent ) && !usedObjectsSet.Contains( parent.gameObject ) ) + searchedTopmostGameObject = parent.gameObject; + } + } + else + { + for( Transform parent = ( (GameObject) obj ).transform.parent; parent != null; parent = parent.parent ) + { + if( objectsToSearchSet.Contains( parent ) ) + { + searchedTopmostGameObject = parent.gameObject; + break; + } + } + } + + if( searchedTopmostGameObject && !string.IsNullOrEmpty( assetPath ) ) // Omit GameObject assets if their parent objects are already included in search + continue; + } + + // Use new ReferenceNodes in UnusedObjects search result group because we don't want these nodes to be linked to the actual ReferenceNodes in any way + // (i.e. we don't use actual ReferenceNodes of these objects (GetReferenceNode) because these may have links to other nodes in unknown circumstances) + ReferenceNode node = PopReferenceNode( obj ); + node.usedState = ReferenceNode.UsedState.Unused; + + if( string.IsNullOrEmpty( assetPath ) ) + { + if( !searchedTopmostGameObject ) + { + if( obj is GameObject ) + unusedMainObjectNodes[obj.GetInstanceID().ToString()] = node; + else + currentSearchResultGroup.AddReference( node ); + } + else // List child GameObject scene objects under their parent GameObject + { + string dictionaryKey = searchedTopmostGameObject.GetInstanceID().ToString(); + List unusedSubObjectNodesAtPath; + if( !unusedSubObjectNodes.TryGetValue( dictionaryKey, out unusedSubObjectNodesAtPath ) ) + unusedSubObjectNodes[dictionaryKey] = unusedSubObjectNodesAtPath = new List( 2 ); + + unusedSubObjectNodesAtPath.Add( node ); + } + } + else + { + if( AssetDatabase.IsMainAsset( obj ) ) + unusedMainObjectNodes[assetPath] = node; + else + { + List unusedSubObjectNodesAtPath; + if( !unusedSubObjectNodes.TryGetValue( assetPath, out unusedSubObjectNodesAtPath ) ) + unusedSubObjectNodes[assetPath] = unusedSubObjectNodesAtPath = new List( 2 ); + + unusedSubObjectNodesAtPath.Add( node ); + } + } + } + + foreach( KeyValuePair kvPair in unusedMainObjectNodes ) + { + List unusedSubAssetNodesAtPath; + if( unusedSubObjectNodes.TryGetValue( kvPair.Key, out unusedSubAssetNodesAtPath ) ) + { + currentSearchResultGroup.AddReference( kvPair.Value ); + for( int i = 0; i < unusedSubAssetNodesAtPath.Count; i++ ) + kvPair.Value.AddLinkTo( unusedSubAssetNodesAtPath[i] ); + + if( usedObjectPathsSet.Contains( kvPair.Key ) ) + kvPair.Value.usedState = ReferenceNode.UsedState.MixedCollapsed; + + unusedSubObjectNodes.Remove( kvPair.Key ); + } + else if( !usedObjectPathsSet.Contains( kvPair.Key ) ) // If a main asset has sub-assets and all of them are used, consider the main asset as used, as well (especially useful for Sprite assets) + currentSearchResultGroup.AddReference( kvPair.Value ); + else if( !AssetDatabase.Contains( (Object) kvPair.Value.nodeObject ) ) + { + currentSearchResultGroup.AddReference( kvPair.Value ); + kvPair.Value.usedState = ReferenceNode.UsedState.MixedCollapsed; + } + } + + foreach( KeyValuePair> kvPair in unusedSubObjectNodes ) // These aren't linked to any unusedMainObjectNodes, add them as root nodes to the search result group + { + foreach( ReferenceNode node in kvPair.Value ) + currentSearchResultGroup.AddReference( node ); + } + + if( currentSearchResultGroup.NumberOfReferences > 0 ) + { + for( int i = 0; i < currentSearchResultGroup.NumberOfReferences; i++ ) + currentSearchResultGroup[i].InitializeRecursively(); + + searchResult.Insert( 0, currentSearchResultGroup ); + } + } + + // Checks if object is asset or scene object and adds it to the corresponding HashSet(s) + private void AddSearchedObjectToFilteredSets( Object obj, bool expandGameObjects ) + { + if( obj == null || obj.Equals( null ) ) + return; + + objectsToSearchSet.Add( obj ); + +#if UNITY_2018_3_OR_NEWER + // When searching for references of a prefab stage object, try adding its corresponding prefab asset to the searched assets, as well + if( openPrefabStage != null && openPrefabStagePrefabAsset != null && obj is GameObject && openPrefabStage.IsPartOfPrefabContents( (GameObject) obj ) ) + { + GameObject prefabStageObjectSource = ( (GameObject) obj ).FollowSymmetricHierarchy( openPrefabStage.prefabContentsRoot, openPrefabStagePrefabAsset ); + if( prefabStageObjectSource != null ) + AddSearchedObjectToFilteredSets( prefabStageObjectSource, expandGameObjects ); + } +#endif + + bool isAsset = obj.IsAsset(); + if( isAsset ) + { + assetsToSearchSet.Add( obj ); + + string assetPath = AssetDatabase.GetAssetPath( obj ); + if( !string.IsNullOrEmpty( assetPath ) ) + { + assetsToSearchPathsSet.Add( assetPath ); + if( searchParameters.dontSearchInSourceAssets && AssetDatabase.IsMainAsset( obj ) ) + excludedAssetsPathsSet.Add( assetPath ); + } + + GameObject go = null; + if( obj is GameObject ) + go = (GameObject) obj; + else if( obj is Component ) + go = ( (Component) obj ).gameObject; + + if( go != null ) + { + Transform transform = go.transform; + bool shouldAddRootPrefabEntry = true; + for( int i = assetsToSearchRootPrefabs.Count - 1; i >= 0; i-- ) + { + Transform rootTransform = assetsToSearchRootPrefabs[i].transform; + if( transform.IsChildOf( rootTransform ) ) + { + shouldAddRootPrefabEntry = false; + break; + } + + if( rootTransform.IsChildOf( transform ) ) + assetsToSearchRootPrefabs.RemoveAt( i ); + } + + if( shouldAddRootPrefabEntry ) + assetsToSearchRootPrefabs.Add( go ); + } + } + else + { + if( obj is GameObject ) + sceneObjectsToSearchScenesSet.Add( ( (GameObject) obj ).scene.path ); + else if( obj is Component ) + sceneObjectsToSearchScenesSet.Add( ( (Component) obj ).gameObject.scene.path ); + } + + if( expandGameObjects && obj is GameObject ) + { + // If searched asset is a GameObject, include its components in the search + Component[] components = ( (GameObject) obj ).GetComponents(); + for( int i = 0; i < components.Length; i++ ) + { + if( components[i] == null || components[i].Equals( null ) ) + continue; + + objectsToSearchSet.Add( components[i] ); + + if( isAsset ) + assetsToSearchSet.Add( components[i] ); + } + } + else if( obj is Component ) + { + // Include searched components' GameObjects in the search, as well + AddSearchedObjectToFilteredSets( ( (Component) obj ).gameObject, false ); + } + } + + // Search a scene for references + private void SearchScene( string scenePath, List searchResult, Parameters searchParameters, SceneSetup[] initialSceneSetup ) + { + Scene scene = EditorSceneManager.GetSceneByPath( scenePath ); + if( isInPlayMode && !scene.isLoaded ) + return; + + bool canContainSceneObjectReference = scene.isLoaded && ( !EditorSceneManager.preventCrossSceneReferences || sceneObjectsToSearchScenesSet.Contains( scenePath ) ); + if( !canContainSceneObjectReference ) + { + bool canContainAssetReference = assetsToSearchSet.Count > 0 && ( isInPlayMode || AssetHasAnyReference( scenePath ) ); + if( !canContainAssetReference ) + return; + } + + if( !scene.isLoaded ) + { + if( searchParameters.lazySceneSearch ) + { + searchResult.Add( new SearchResultGroup( scenePath, SearchResultGroup.GroupType.Scene, true, true ) ); + return; + } + + scene = EditorSceneManager.OpenScene( scenePath, OpenSceneMode.Additive ); + } + + currentSearchResultGroup = new SearchResultGroup( scenePath, SearchResultGroup.GroupType.Scene ); + + // Search through all the GameObjects in the scene + GameObject[] rootGameObjects = scene.GetRootGameObjects(); + for( int i = 0; i < rootGameObjects.Length; i++ ) + SearchGameObjectRecursively( rootGameObjects[i] ); + + // Search through Lighting Settings (it requires changing the active scene but don't do that in play mode) + if( searchParameters.searchInSceneLightingSettings && ( !isInPlayMode || SceneManager.GetActiveScene() == scene ) ) + { + if( !isInPlayMode && EditorSceneManager.GetActiveScene() != scene ) + EditorSceneManager.SetActiveScene( scene ); + + BeginSearchObject( lightmapSettingsGetter() ); + BeginSearchObject( renderSettingsGetter() ); + } + + // If no references are found in the scene and if the scene is not part of the initial scene setup, close it + if( currentSearchResultGroup.NumberOfReferences == 0 ) + { + if( !isInPlayMode ) + { + bool sceneIsOneOfInitials = false; + for( int i = 0; i < initialSceneSetup.Length; i++ ) + { + if( initialSceneSetup[i].path == scenePath ) + { + if( !initialSceneSetup[i].isLoaded ) + EditorSceneManager.CloseScene( scene, false ); + + sceneIsOneOfInitials = true; + break; + } + } + + if( !sceneIsOneOfInitials ) + EditorSceneManager.CloseScene( scene, true ); + } + } + else + { + // Some references are found in this scene, save the results + searchResult.Add( currentSearchResultGroup ); + } + } + + // Search a GameObject and its children for references recursively + private void SearchGameObjectRecursively( GameObject go ) + { + BeginSearchObject( go ); + + Transform tr = go.transform; + for( int i = 0; i < tr.childCount; i++ ) + SearchGameObjectRecursively( tr.GetChild( i ).gameObject ); + } + + // Begin searching a root object (like a GameObject or an asset) + private void BeginSearchObject( Object obj ) + { + if( obj is SceneAsset ) + return; + + currentSearchedObject = obj; + + ReferenceNode searchResult = SearchObject( obj ); + if( searchResult != null ) + currentSearchResultGroup.AddReference( searchResult ); + } + + // Search an object for references + private ReferenceNode SearchObject( object obj ) + { + if( obj == null || obj.Equals( null ) ) + return null; + + // Avoid recursion (which leads to stackoverflow exception) using a stack (initially, I was using callStack.ContainsFast + // here but it returned false for objects that do exist in the call stack if VFX Graph window was open) + for( int i = callStack.Count - 1; i >= 0; i-- ) + { + if( callStack[i].Equals( obj ) ) + return null; + } + + bool searchingSourceAsset = searchingSourceAssets && ReferenceEquals( currentSearchedObject, obj ); + + // Hashing does not work well with structs all the time, don't cache search results for structs + if( !( obj is ValueType ) && !searchingSourceAsset ) + { + // If object was searched before, return the cached result + ReferenceNode cachedResult; + if( TryGetReferenceNode( obj, out cachedResult ) ) + return cachedResult; + } + + searchedObjectsCount++; + + ReferenceNode result; + Object unityObject = obj as Object; + if( unityObject != null ) + { + // If the Object is an asset, search it in detail only if its dependencies contain at least one of the searched asset(s) + string assetPath = null; + if( unityObject.IsAsset() ) + { + if( assetsToSearchSet.Count == 0 ) + { + searchedUnityObjects.Add( unityObject.GetInstanceID(), null ); + return null; + } + + assetPath = AssetDatabase.GetAssetPath( unityObject ); + if( excludedAssetsPathsSet.Contains( assetPath ) || !AssetHasAnyReference( assetPath ) ) + { + searchedUnityObjects.Add( unityObject.GetInstanceID(), null ); + return null; + } + } + + callStack.Add( unityObject ); + + // Search the Object in detail + Func searchFunction; + if( assetPath != null && extensionToSearchFunction.TryGetValue( Utilities.GetFileExtension( assetPath ), out searchFunction ) && AssetDatabase.IsMainAsset( unityObject ) ) + result = searchFunction( unityObject ); + else if( typeToSearchFunction.TryGetValue( unityObject.GetType(), out searchFunction ) ) + result = searchFunction( unityObject ); + else if( unityObject is Component ) + result = SearchComponent( unityObject ); + else + { + result = PopReferenceNode( unityObject ); + SearchVariablesWithSerializedObject( result ); + } + + // A prefab asset should have a link to its children because when a scene object uses a prefab and a child of that prefab uses + // a searched object, the scene object needs to appear in the search results. Since prefab assets aren't automatically linked to + // their children, we need to create that link manually + if( assetPath != null && unityObject is GameObject && AssetDatabase.IsMainAsset( unityObject ) ) + { + if( result == null ) + result = PopReferenceNode( unityObject ); + + GameObject prefabGameObject = (GameObject) unityObject; + Transform[] prefabChildren = prefabGameObject.GetComponentsInChildren( true ); + for( int i = 0; i < prefabChildren.Length; i++ ) + { + if( prefabChildren[i].gameObject != prefabGameObject ) + result.AddLinkTo( SearchObject( prefabChildren[i].gameObject ), isWeakLink: true ); + } + } + + callStack.RemoveAt( callStack.Count - 1 ); + } + else + { + // Comply with the recursive search limit + if( currentDepth >= searchParameters.searchDepthLimit ) + return null; + + callStack.Add( obj ); + currentDepth++; + + result = PopReferenceNode( obj ); + SearchVariablesWithReflection( result ); + + currentDepth--; + callStack.RemoveAt( callStack.Count - 1 ); + } + + if( result != null && result.NumberOfOutgoingLinks == 0 ) + { + PoolReferenceNode( result ); + result = null; + } + + // Cache the search result if we are skimming through a class (not a struct; i.e. objHash != null) + // and if the object is a UnityEngine.Object (if not, cache the result only if we have actually found something + // or we are at the root of the search; i.e. currentDepth == 0) + if( !( obj is ValueType ) && ( result != null || unityObject != null || currentDepth == 0 ) ) + { + if( !searchingSourceAsset ) + { + if( obj is Object ) + searchedUnityObjects.Add( unityObject.GetInstanceID(), result ); + else + searchedObjects.Add( GetNodeObjectHash( obj ), result ); + } + else if( result != null ) + { + result.CopyReferencesTo( searchedUnityObjects[unityObject.GetInstanceID()] ); + PoolReferenceNode( result ); + } + } + + return result; + } + + // Check if the asset at specified path depends on any of the references + private bool AssetHasAnyReference( string assetPath ) + { +#if ASSET_USAGE_ADDRESSABLES + if( searchParameters.addressablesSupport ) + return true; +#endif + + if( assetsToSearchPathsSet.Contains( assetPath ) ) + return true; + + if( alwaysSearchedExtensionsSet.Count > 0 && alwaysSearchedExtensionsSet.Contains( Utilities.GetFileExtension( assetPath ) ) ) + return true; + + return AssetHasAnyReferenceInternal( assetPath ); + } + + // Recursively check if the asset at specified path depends on any of the references + private bool AssetHasAnyReferenceInternal( string assetPath ) + { + CacheEntry cacheEntry; + if( !assetDependencyCache.TryGetValue( assetPath, out cacheEntry ) ) + { + cacheEntry = new CacheEntry( assetPath ); + assetDependencyCache[assetPath] = cacheEntry; + } + else if( !cacheEntry.verified ) + cacheEntry.Verify( assetPath ); + + if( cacheEntry.searchResult != CacheEntry.Result.Unknown ) + return cacheEntry.searchResult == CacheEntry.Result.Yes; + + cacheEntry.searchResult = CacheEntry.Result.No; + + string[] dependencies = cacheEntry.dependencies; + long[] fileSizes = cacheEntry.fileSizes; + for( int i = 0; i < dependencies.Length; i++ ) + { + // If a dependency was renamed (which doesn't affect the verified hash, unfortunately), + // force refresh the asset's dependencies and search it again + if( !Directory.Exists( dependencies[i] ) ) // Calling FileInfo.Length on a directory throws FileNotFoundException + { + FileInfo assetFile = new FileInfo( dependencies[i] ); + if( !assetFile.Exists || assetFile.Length != fileSizes[i] ) + { + // Although not reproduced, it is reported that this section caused StackOverflowException due to infinite loop, + // if that happens, log useful information to help reproduce the issue + if( lastRefreshedCacheEntry == cacheEntry ) + { + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + sb.EnsureCapacity( 1000 ); + + sb.AppendLine( "Infinite loop while refreshing a cache entry, please report it to the author." ).AppendLine(); + sb.Append( "Asset path: " ).AppendLine( assetPath ); + + for( int j = 0; j < 2; j++ ) + { + if( j == 1 ) + { + cacheEntry.Refresh( assetPath ); + dependencies = cacheEntry.dependencies; + fileSizes = cacheEntry.fileSizes; + } + + sb.AppendLine().AppendLine( j == 0 ? "Old Dependencies:" : "New Dependencies" ); + for( int k = 0; k < dependencies.Length; k++ ) + { + sb.Append( "- " ).Append( dependencies[k] ); + + if( Directory.Exists( dependencies[k] ) ) + { + sb.Append( " (Dir)" ); + if( fileSizes[k] != 0L ) + sb.Append( " WasCachedAsFile: " ).Append( fileSizes[k] ); + } + else + { + assetFile = new FileInfo( dependencies[k] ); + sb.Append( " (File) " ).Append( "CachedSize: " ).Append( fileSizes[k] ); + if( assetFile.Exists ) + sb.Append( " RealSize: " ).Append( assetFile.Length ); + else + sb.Append( " NoLongerExists" ); + } + + sb.AppendLine(); + } + } + + Debug.LogError( sb.ToString() ); + return false; + } + + cacheEntry.Refresh( assetPath ); + cacheEntry.searchResult = CacheEntry.Result.Unknown; + lastRefreshedCacheEntry = cacheEntry; + + return AssetHasAnyReferenceInternal( assetPath ); + } + } + + if( assetsToSearchPathsSet.Contains( dependencies[i] ) ) + { + cacheEntry.searchResult = CacheEntry.Result.Yes; + return true; + } + } + + for( int i = 0; i < dependencies.Length; i++ ) + { + if( AssetHasAnyReferenceInternal( dependencies[i] ) ) + { + cacheEntry.searchResult = CacheEntry.Result.Yes; + return true; + } + } + + return false; + } + + // If object was already searched, return its ReferenceNode + private bool TryGetReferenceNode( object nodeObject, out ReferenceNode referenceNode ) + { + if( nodeObject is Object ) + { + if( searchedUnityObjects.TryGetValue( ( (Object) nodeObject ).GetInstanceID(), out referenceNode ) ) + return true; + } + else if( searchedObjects.TryGetValue( GetNodeObjectHash( nodeObject ), out referenceNode ) ) + return true; + + referenceNode = null; + return false; + } + + // Get reference node for object + private ReferenceNode GetReferenceNode( object nodeObject ) + { + ReferenceNode result; + if( nodeObject is Object ) + { + int hash = ( (Object) nodeObject ).GetInstanceID(); + if( !searchedUnityObjects.TryGetValue( hash, out result ) || result == null ) + { + result = PopReferenceNode( nodeObject ); + searchedUnityObjects[hash] = result; + } + } + else + { + string hash = GetNodeObjectHash( nodeObject ); + if( !searchedObjects.TryGetValue( hash, out result ) || result == null ) + { + result = PopReferenceNode( nodeObject ); + searchedObjects[hash] = result; + } + } + + return result; + } + + // Fetch a reference node from pool + private ReferenceNode PopReferenceNode( object nodeObject ) + { + ReferenceNode node; + if( nodesPool.Count == 0 ) + node = new ReferenceNode(); + else + { + int index = nodesPool.Count - 1; + node = nodesPool[index]; + nodesPool.RemoveAt( index ); + } + + node.nodeObject = nodeObject; + return node; + } + + // Pool a reference node + private void PoolReferenceNode( ReferenceNode node ) + { + node.Clear(); + nodesPool.Add( node ); + } + + // Get a unique-ish string hash code for a plain C# object (i.e. non-UnityEngine.Object object) + private string GetNodeObjectHash( object nodeObject ) + { + return nodeObject.GetHashCode() + nodeObject.GetType().Name; + } + + // Retrieve the game objects listed under the DontDestroyOnLoad scene + private GameObject[] GetDontDestroyOnLoadObjects() + { + GameObject temp = null; + try + { + temp = new GameObject(); + Object.DontDestroyOnLoad( temp ); + Scene dontDestroyOnLoad = temp.scene; + Object.DestroyImmediate( temp ); + temp = null; + + return dontDestroyOnLoad.GetRootGameObjects(); + } + finally + { + if( temp != null ) + Object.DestroyImmediate( temp ); + } + } + + // Appends contents of callStack to StringBuilder and returns the most recent Unity object in callStack + private Object AppendCallStackToStringBuilder( StringBuilder sb ) + { + Object latestUnityObjectInCallStack = null; + if( callStack.Count > 0 ) + { + sb.AppendLine().AppendLine( "Stack contents: " ); + + for( int i = callStack.Count - 1; i >= 0; i-- ) + { + latestUnityObjectInCallStack = callStack[i] as Object; + if( latestUnityObjectInCallStack ) + { + if( !AssetDatabase.Contains( latestUnityObjectInCallStack ) ) + { + string scenePath = AssetDatabase.GetAssetOrScenePath( latestUnityObjectInCallStack ); + if( !string.IsNullOrEmpty( scenePath ) && SceneManager.GetSceneByPath( scenePath ).IsValid() ) + sb.Append( "Scene: " ).AppendLine( scenePath ); + } + + break; + } + } + + for( int i = callStack.Count - 1; i >= 0; i-- ) + { + sb.Append( i ).Append( ": " ); + + Object unityObject = callStack[i] as Object; + if( unityObject ) + sb.Append( unityObject.name ).Append( " (" ).Append( unityObject.GetType() ).AppendLine( ")" ); + else if( callStack[i] != null ) + sb.Append( callStack[i].GetType() ).AppendLine( " object" ); + else + sb.AppendLine( "<>" ); + } + + sb.AppendLine(); + } + + return latestUnityObjectInCallStack; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs.meta new file mode 100644 index 0000000..8d63a0c --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2c0dea52dcdb16e4e9b13f8dacc1590f +timeCreated: 1520032279 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorCache.cs b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorCache.cs new file mode 100644 index 0000000..4bb1403 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorCache.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace AssetUsageDetectorNamespace +{ + public partial class AssetUsageDetector + { + #region Helper Classes + private class CacheEntry + { + public enum Result { Unknown = 0, No = 1, Yes = 2 }; + + public string hash; + public string[] dependencies; + public long[] fileSizes; + + public bool verified; + public Result searchResult; + + public CacheEntry( string path ) + { + Verify( path ); + } + + public CacheEntry( string hash, string[] dependencies, long[] fileSizes ) + { + this.hash = hash; + this.dependencies = dependencies; + this.fileSizes = fileSizes; + } + + public void Verify( string path ) + { + string hash = AssetDatabase.GetAssetDependencyHash( path ).ToString(); + if( this.hash != hash ) + { + this.hash = hash; + Refresh( path ); + } + + verified = true; + } + + public void Refresh( string path ) + { + dependencies = AssetDatabase.GetDependencies( path, false ); + if( fileSizes == null || fileSizes.Length != dependencies.Length ) + fileSizes = new long[dependencies.Length]; + + int length = dependencies.Length; + for( int i = 0; i < length; i++ ) + { + if( !string.IsNullOrEmpty( dependencies[i] ) ) + { + FileInfo assetFile = new FileInfo( dependencies[i] ); + fileSizes[i] = assetFile.Exists ? assetFile.Length : 0L; + } + else + { + // This dependency is empty which causes issues when passed to FileInfo constructor + // Find a non-empty dependency and move it to this index + for( int j = length - 1; j > i; j--, length-- ) + { + if( !string.IsNullOrEmpty( dependencies[j] ) ) + { + dependencies[i--] = dependencies[j]; + break; + } + } + + length--; + } + } + + if( length != fileSizes.Length ) + { + Array.Resize( ref dependencies, length ); + Array.Resize( ref fileSizes, length ); + } + } + } + #endregion + + // An optimization to fetch the dependencies of an asset only once (key is the path of the asset) + private Dictionary assetDependencyCache; + private CacheEntry lastRefreshedCacheEntry; + + private string CachePath { get { return Application.dataPath + "/../Library/AssetUsageDetector.cache"; } } // Path of the cache file + + public void SaveCache() + { + if( assetDependencyCache == null ) + return; + + try + { + using( FileStream stream = new FileStream( CachePath, FileMode.Create ) ) + using( BinaryWriter writer = new BinaryWriter( stream ) ) + { + writer.Write( assetDependencyCache.Count ); + + foreach( var keyValuePair in assetDependencyCache ) + { + CacheEntry cacheEntry = keyValuePair.Value; + string[] dependencies = cacheEntry.dependencies; + long[] fileSizes = cacheEntry.fileSizes; + + writer.Write( keyValuePair.Key ); + writer.Write( cacheEntry.hash ); + writer.Write( dependencies.Length ); + + for( int i = 0; i < dependencies.Length; i++ ) + { + writer.Write( dependencies[i] ); + writer.Write( fileSizes[i] ); + } + } + } + } + catch( Exception e ) + { + Debug.LogException( e ); + } + } + + private void LoadCache() + { + if( File.Exists( CachePath ) ) + { + using( FileStream stream = new FileStream( CachePath, FileMode.Open, FileAccess.Read ) ) + using( BinaryReader reader = new BinaryReader( stream ) ) + { + try + { + int cacheSize = reader.ReadInt32(); + assetDependencyCache = new Dictionary( cacheSize ); + + for( int i = 0; i < cacheSize; i++ ) + { + string assetPath = reader.ReadString(); + string hash = reader.ReadString(); + + int dependenciesLength = reader.ReadInt32(); + string[] dependencies = new string[dependenciesLength]; + long[] fileSizes = new long[dependenciesLength]; + for( int j = 0; j < dependenciesLength; j++ ) + { + dependencies[j] = reader.ReadString(); + fileSizes[j] = reader.ReadInt64(); + } + + assetDependencyCache[assetPath] = new CacheEntry( hash, dependencies, fileSizes ); + } + } + catch( Exception e ) + { + assetDependencyCache = null; + Debug.LogWarning( "Couldn't load cache (probably cache format has changed in an update), will regenerate cache.\n" + e.ToString() ); + } + } + } + + // Generate cache for all assets for the first time + if( assetDependencyCache == null ) + { + assetDependencyCache = new Dictionary( 1024 * 8 ); + + string[] allAssets = AssetDatabase.GetAllAssetPaths(); + if( allAssets.Length > 0 ) + { + double startTime = EditorApplication.timeSinceStartup; + + try + { + for( int i = 0; i < allAssets.Length; i++ ) + { + if( i % 30 == 0 && EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Generating cache for the first time (optional)", (float) i / allAssets.Length ) ) + { + EditorUtility.ClearProgressBar(); + Debug.LogWarning( "Initial cache generation cancelled, cache will be generated on the fly as more and more assets are searched." ); + break; + } + + assetDependencyCache[allAssets[i]] = new CacheEntry( allAssets[i] ); + } + + EditorUtility.ClearProgressBar(); + + Debug.Log( "Cache generated in " + ( EditorApplication.timeSinceStartup - startTime ).ToString( "F2" ) + " seconds" ); + Debug.Log( "You can always reset the cache by deleting " + Path.GetFullPath( CachePath ) ); + + SaveCache(); + } + catch( Exception e ) + { + EditorUtility.ClearProgressBar(); + Debug.LogException( e ); + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorCache.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorCache.cs.meta new file mode 100644 index 0000000..3d8d896 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 71ea9a3fd0b82594d8130d882dbfc844 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSearchFunctions.cs b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSearchFunctions.cs new file mode 100644 index 0000000..e978d58 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSearchFunctions.cs @@ -0,0 +1,1962 @@ +using System; +using System.Collections.Generic; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Text; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; +using UnityEngine.UI; +#if UNITY_2017_1_OR_NEWER +using UnityEngine.U2D; +using UnityEngine.Playables; +#endif +#if UNITY_2018_2_OR_NEWER +using UnityEditor.U2D; +#endif +#if UNITY_2017_3_OR_NEWER +using UnityEditor.Compilation; +#endif +#if UNITY_2017_2_OR_NEWER +using UnityEngine.Tilemaps; +#endif +#if ASSET_USAGE_ADDRESSABLES +using UnityEngine.AddressableAssets; +#endif +using Object = UnityEngine.Object; + +namespace AssetUsageDetectorNamespace +{ + public partial class AssetUsageDetector + { + #region Helper Classes +#if UNITY_2017_3_OR_NEWER +#pragma warning disable 0649 // The fields' values are assigned via JsonUtility + [Serializable] + private struct AssemblyDefinitionReferences + { + public string reference; // Used by AssemblyDefinitionReferenceAssets + public List references; // Used by AssemblyDefinitionAssets + } +#pragma warning restore 0649 +#endif + +#if UNITY_2018_1_OR_NEWER +#pragma warning disable 0649 // The fields' values are assigned via JsonUtility + [Serializable] + private struct ShaderGraphReferences // Used by old Shader Graph serialization format + { + [Serializable] + public struct JSONHolder + { + public string JSONnodeData; + } + + [Serializable] + public class TextureHolder + { + public string m_SerializedTexture; + public string m_SerializedCubemap; + public string m_Guid; + + public string GetTexturePath() + { + string guid = ExtractGUIDFromString( !string.IsNullOrEmpty( m_SerializedTexture ) ? m_SerializedTexture : m_SerializedCubemap ); + if( string.IsNullOrEmpty( guid ) ) + guid = m_Guid; + + return string.IsNullOrEmpty( guid ) ? null : AssetDatabase.GUIDToAssetPath( guid ); + } + } + + [Serializable] + public struct PropertyData + { + public string m_Name; + public string m_DefaultReferenceName; + public string m_OverrideReferenceName; + public TextureHolder m_Value; + + public string GetName() + { + if( !string.IsNullOrEmpty( m_OverrideReferenceName ) ) + return m_OverrideReferenceName; + if( !string.IsNullOrEmpty( m_DefaultReferenceName ) ) + return m_DefaultReferenceName; + if( !string.IsNullOrEmpty( m_Name ) ) + return m_Name; + + return "Property"; + } + } + + [Serializable] + public struct NodeData + { + public string m_Name; + public string m_FunctionSource; // Custom Function node's Source field + public string m_SerializedSubGraph; // Sub-graph node + public List m_SerializableSlots; + + public string GetSubGraphPath() + { + string guid = ExtractGUIDFromString( m_SerializedSubGraph ); + return string.IsNullOrEmpty( guid ) ? null : AssetDatabase.GUIDToAssetPath( guid ); + } + } + + [Serializable] + public struct NodeSlotData + { + public TextureHolder m_Texture; + public TextureHolder m_TextureArray; + public TextureHolder m_Cubemap; + + public string GetTexturePath() + { + if( m_Texture != null ) + return m_Texture.GetTexturePath(); + if( m_Cubemap != null ) + return m_Cubemap.GetTexturePath(); + if( m_TextureArray != null ) + return m_TextureArray.GetTexturePath(); + + return null; + } + } + + public List m_SerializedProperties; + public List m_SerializableNodes; + + // String can be in one of the following formats: + // "guid":"GUID_VALUE" + // "guid": "GUID_VALUE" + // "guid" : "GUID_VALUE" + private static string ExtractGUIDFromString( string str ) + { + if( !string.IsNullOrEmpty( str ) ) + { + int guidStartIndex = str.IndexOf( "\"guid\"" ); + if( guidStartIndex >= 0 ) + { + guidStartIndex += 6; + guidStartIndex = str.IndexOf( '"', guidStartIndex ); + if( guidStartIndex > 0 ) + { + guidStartIndex++; + + int guidEndIndex = str.IndexOf( '"', guidStartIndex ); + if( guidEndIndex > 0 ) + return str.Substring( guidStartIndex, guidEndIndex - guidStartIndex ); + } + } + } + + return null; + } + } +#pragma warning restore 0649 +#endif + #endregion + + // Dictionary to quickly find the function to search a specific type with + private Dictionary> typeToSearchFunction; + // Dictionary to associate special file extensions with their search functions + private Dictionary> extensionToSearchFunction; + + // An optimization to fetch & filter fields and properties of a class only once + private readonly Dictionary typeToVariables = new Dictionary( 4096 ); + private readonly List validVariables = new List( 32 ); + + // All MonoScripts in objectsToSearchSet + private readonly List monoScriptsToSearch = new List(); + private readonly List monoScriptsToSearchTypes = new List(); + + // Path(s) of .cginc, .cg, .hlsl and .glslinc assets in assetsToSearchSet + private readonly HashSet shaderIncludesToSearchSet = new HashSet(); + +#if UNITY_2017_3_OR_NEWER + // Path(s) of the Assembly Definition Files in objectsToSearchSet (Value: files themselves) + private readonly Dictionary assemblyDefinitionFilesToSearch = new Dictionary( 8 ); +#endif + + // An optimization to fetch an animation clip's curve bindings only once + private readonly Dictionary animationClipUniqueBindings = new Dictionary( 256 ); + + private bool searchPrefabConnections; + private bool searchMonoBehavioursForScript; + private bool searchTextureReferences; +#if UNITY_2018_1_OR_NEWER + private bool searchShaderGraphsForSubGraphs; +#endif + + private bool searchSerializableVariablesOnly; + private bool prevSearchSerializableVariablesOnly; + + private BindingFlags fieldModifiers, propertyModifiers; + private BindingFlags prevFieldModifiers, prevPropertyModifiers; + + // Unity's internal function that returns a SerializedProperty's corresponding FieldInfo + private delegate FieldInfo FieldInfoGetter( SerializedProperty p, out Type t ); +#if UNITY_2019_3_OR_NEWER + private readonly FieldInfoGetter fieldInfoGetter = (FieldInfoGetter) Delegate.CreateDelegate( typeof( FieldInfoGetter ), typeof( Editor ).Assembly.GetType( "UnityEditor.ScriptAttributeUtility" ).GetMethod( "GetFieldInfoAndStaticTypeFromProperty", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) ); +#else + private readonly FieldInfoGetter fieldInfoGetter = (FieldInfoGetter) Delegate.CreateDelegate( typeof( FieldInfoGetter ), typeof( Editor ).Assembly.GetType( "UnityEditor.ScriptAttributeUtility" ).GetMethod( "GetFieldInfoFromProperty", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) ); +#endif + + private readonly Func lightmapSettingsGetter = (Func) Delegate.CreateDelegate( typeof( Func ), typeof( LightmapEditorSettings ).GetMethod( "GetLightmapSettings", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) ); + private readonly Func renderSettingsGetter = (Func) Delegate.CreateDelegate( typeof( Func ), typeof( RenderSettings ).GetMethod( "GetRenderSettings", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) ); +#if UNITY_2021_2_OR_NEWER + private readonly Func defaultReflectionProbeGetter = (Func) Delegate.CreateDelegate( typeof( Func ), typeof( RenderSettings ).GetProperty( "defaultReflection", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).GetGetMethod( true ) ); +#endif + +#if ASSET_USAGE_ADDRESSABLES + private readonly Func spriteAtlasPackedSpritesGetter = (Func) Delegate.CreateDelegate( typeof( Func ), typeof( SpriteAtlasExtensions ).GetMethod( "GetPackedSprites", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) ); + private readonly PropertyInfo assetReferenceSubObjectTypeGetter = typeof( AssetReference ).GetProperty( "SubOjbectType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ); +#endif + +#if ASSET_USAGE_VFX_GRAPH + private static Type vfxResourceType => typeof( Editor ).Assembly.GetType( "UnityEditor.VFX.VisualEffectResource" ) ?? Array.Find( AppDomain.CurrentDomain.GetAssemblies(), ( assembly ) => assembly.GetName().Name == "UnityEditor.VFXModule" ).GetType( "UnityEditor.VFX.VisualEffectResource" ); + private readonly Func vfxResourceGetter = (Func) Delegate.CreateDelegate( typeof( Func ), vfxResourceType.GetMethod( "GetResourceAtPath", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) ); + private readonly MethodInfo vfxResourceContentsGetter = vfxResourceType.GetMethod( "GetContents", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ); + private readonly MethodInfo vfxSerializableObjectValueGetter = Array.Find( Array.Find( AppDomain.CurrentDomain.GetAssemblies(), ( assembly ) => assembly.GetName().Name == "Unity.VisualEffectGraph.Editor" ).GetType( "UnityEditor.VFX.VFXSerializableObject" ).GetMethods( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ), ( methodInfo ) => methodInfo.Name == "Get" && !methodInfo.IsGenericMethod ); +#endif + + private void InitializeSearchFunctionsData( Parameters searchParameters ) + { + if( typeToSearchFunction == null ) + { + typeToSearchFunction = new Dictionary>() + { + { typeof( GameObject ), SearchGameObject }, + { typeof( Material ), SearchMaterial }, + { typeof( Shader ), SearchShader }, + { typeof( MonoScript ), SearchMonoScript }, + { typeof( RuntimeAnimatorController ), SearchAnimatorController }, + { typeof( AnimatorOverrideController ), SearchAnimatorController }, + { typeof( AnimatorController ), SearchAnimatorController }, + { typeof( AnimatorStateMachine ), SearchAnimatorStateMachine }, + { typeof( AnimatorState ), SearchAnimatorState }, + { typeof( AnimatorStateTransition ), SearchAnimatorStateTransition }, + { typeof( BlendTree ), SearchBlendTree }, + { typeof( AnimationClip ), SearchAnimationClip }, + { typeof( TerrainData ), SearchTerrainData }, + { typeof( LightmapSettings ), SearchLightmapSettings }, + { typeof( RenderSettings ), SearchRenderSettings }, +#if UNITY_2017_1_OR_NEWER + { typeof( SpriteAtlas ), SearchSpriteAtlas }, +#endif + }; + } + + if( extensionToSearchFunction == null ) + { + extensionToSearchFunction = new Dictionary>() + { + { "compute", SearchShaderSecondaryAsset }, + { "cginc", SearchShaderSecondaryAsset }, + { "cg", SearchShaderSecondaryAsset }, + { "glslinc", SearchShaderSecondaryAsset }, + { "hlsl", SearchShaderSecondaryAsset }, +#if UNITY_2017_3_OR_NEWER + { "asmdef", SearchAssemblyDefinitionFile }, +#endif +#if UNITY_2019_2_OR_NEWER + { "asmref", SearchAssemblyDefinitionFile }, +#endif +#if UNITY_2018_1_OR_NEWER + { "shadergraph", SearchShaderGraph }, + { "shadersubgraph", SearchShaderGraph }, +#endif +#if ASSET_USAGE_VFX_GRAPH + { "vfx", SearchVFXGraphAsset }, + { "vfxoperator", SearchVFXGraphAsset }, + { "vfxblock", SearchVFXGraphAsset }, +#endif + }; + } + + fieldModifiers = searchParameters.fieldModifiers | BindingFlags.Instance | BindingFlags.DeclaredOnly; + propertyModifiers = searchParameters.propertyModifiers | BindingFlags.Instance | BindingFlags.DeclaredOnly; + searchSerializableVariablesOnly = !searchParameters.searchNonSerializableVariables; + + if( prevFieldModifiers != fieldModifiers || prevPropertyModifiers != propertyModifiers || prevSearchSerializableVariablesOnly != searchSerializableVariablesOnly ) + typeToVariables.Clear(); + + prevFieldModifiers = fieldModifiers; + prevPropertyModifiers = propertyModifiers; + prevSearchSerializableVariablesOnly = searchSerializableVariablesOnly; + + searchPrefabConnections = false; + searchMonoBehavioursForScript = false; + searchTextureReferences = false; +#if UNITY_2018_1_OR_NEWER + searchShaderGraphsForSubGraphs = false; +#endif +#if ASSET_USAGE_VFX_GRAPH + bool searchVFXGraphs = false; +#endif + + foreach( Object obj in objectsToSearchSet ) + { + if( obj is Texture || obj is Sprite ) + searchTextureReferences = true; + else if( obj is MonoScript ) + { + searchMonoBehavioursForScript = true; + + Type monoScriptType = ( (MonoScript) obj ).GetClass(); + if( monoScriptType != null && !monoScriptType.IsSealed ) + { + monoScriptsToSearch.Add( (MonoScript) obj ); + monoScriptsToSearchTypes.Add( monoScriptType ); + } + } + else if( obj is GameObject ) + searchPrefabConnections = true; +#if UNITY_2017_3_OR_NEWER + else if( obj is UnityEditorInternal.AssemblyDefinitionAsset ) + assemblyDefinitionFilesToSearch[AssetDatabase.GetAssetPath( obj )] = obj; +#endif +#if ASSET_USAGE_VFX_GRAPH + else if( !searchVFXGraphs && ( obj is Shader || obj is Mesh || obj.GetType().Name.StartsWithFast( "PointCache" ) || obj.GetType().Name == "ShaderGraphVfxAsset" ) ) + searchVFXGraphs = true; +#endif + } + + // We need to search for class/interface inheritance references manually because AssetDatabase.GetDependencies doesn't take that into account + if( monoScriptsToSearch.Count > 0 ) + { + alwaysSearchedExtensionsSet.Add( "cs" ); + alwaysSearchedExtensionsSet.Add( "dll" ); + } + + foreach( string path in assetsToSearchPathsSet ) + { + string extension = Utilities.GetFileExtension( path ); + if( extension == "hlsl" || extension == "cginc" || extension == "cg" || extension == "glslinc" ) + shaderIncludesToSearchSet.Add( path ); +#if UNITY_2018_1_OR_NEWER + else if( extension == "shadersubgraph" ) + searchShaderGraphsForSubGraphs = true; +#endif + } + + // AssetDatabase.GetDependencies doesn't take #include lines in shader source codes into consideration. If we are searching for references + // of a potential #include target (shaderIncludesToSearchSet), we must search all shader assets and check their #include lines manually + if( shaderIncludesToSearchSet.Count > 0 ) + { + alwaysSearchedExtensionsSet.Add( "shader" ); + alwaysSearchedExtensionsSet.Add( "compute" ); + alwaysSearchedExtensionsSet.Add( "cginc" ); + alwaysSearchedExtensionsSet.Add( "cg" ); + alwaysSearchedExtensionsSet.Add( "glslinc" ); + alwaysSearchedExtensionsSet.Add( "hlsl" ); + } + +#if UNITY_2017_3_OR_NEWER + // AssetDatabase.GetDependencies doesn't return references from Assembly Definition Files to their Assembly Definition References, + // so if we are searching for an Assembly Definition File's usages, we must search all Assembly Definition Files' references manually. + if( assemblyDefinitionFilesToSearch.Count > 0 ) + { + alwaysSearchedExtensionsSet.Add( "asmdef" ); +#if UNITY_2019_2_OR_NEWER + alwaysSearchedExtensionsSet.Add( "asmref" ); +#endif + } +#endif + +#if UNITY_2018_1_OR_NEWER + // AssetDatabase.GetDependencies doesn't work with Shader Graph assets. We must search all Shader Graph assets in the following cases: + // searchTextureReferences: to find Texture references used in various nodes and properties + // searchShaderGraphsForSubGraphs: to find Shader Sub-graph references in other Shader Graph assets + // shaderIncludesToSearchSet: to find .cginc, .cg, .glslinc and .hlsl references used in Custom Function nodes + if( searchTextureReferences || searchShaderGraphsForSubGraphs || shaderIncludesToSearchSet.Count > 0 ) + { + alwaysSearchedExtensionsSet.Add( "shadergraph" ); + alwaysSearchedExtensionsSet.Add( "shadersubgraph" ); + } +#endif + +#if ASSET_USAGE_VFX_GRAPH + if( searchTextureReferences || searchVFXGraphs ) + { + alwaysSearchedExtensionsSet.Add( "vfx" ); + alwaysSearchedExtensionsSet.Add( "vfxoperator" ); + alwaysSearchedExtensionsSet.Add( "vfxblock" ); + } +#endif + } + + private ReferenceNode SearchGameObject( object obj ) + { + GameObject go = (GameObject) obj; + ReferenceNode referenceNode = PopReferenceNode( go ); + + // Check if this GameObject's prefab is one of the selected assets + if( searchPrefabConnections ) + { +#if UNITY_2018_3_OR_NEWER + Object prefab = go; + while( prefab = PrefabUtility.GetCorrespondingObjectFromSource( prefab ) ) +#else + Object prefab = PrefabUtility.GetPrefabParent( go ); + if( prefab ) +#endif + { + if( objectsToSearchSet.Contains( prefab ) && assetsToSearchRootPrefabs.ContainsFast( prefab as GameObject ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( prefab ), "Prefab object" ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new PrefabMatch( go, prefab ) ); + } + } + } + + // Search through all the components of the object + Component[] components = go.GetComponents(); + for( int i = 0; i < components.Length; i++ ) + referenceNode.AddLinkTo( SearchObject( components[i] ), isWeakLink: true ); + + return referenceNode; + } + + private ReferenceNode SearchComponent( object obj ) + { + Component component = (Component) obj; + + // Ignore Transform component (no object field to search for) + if( component is Transform ) + return null; + + ReferenceNode referenceNode = PopReferenceNode( component ); + + if( searchMonoBehavioursForScript && component is MonoBehaviour ) + { + // If a searched asset is script, check if this component is an instance of it + // Although SearchVariablesWithSerializedObject can detect these references with SerializedObject, it isn't possible when reflection is used in Play mode + MonoScript script = MonoScript.FromMonoBehaviour( (MonoBehaviour) component ); + if( objectsToSearchSet.Contains( script ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( script ) ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new BehaviourUsageMatch( component.gameObject, script, component ) ); + } + } + + if( component is Animation ) + { + // Search animation clips for references + if( searchParameters.searchRefactoring == null ) + { + foreach( AnimationState anim in (Animation) component ) + referenceNode.AddLinkTo( SearchObject( anim.clip ) ); + } + else + { + AnimationClip[] clips = AnimationUtility.GetAnimationClips( component.gameObject ); + bool modifiedClips = false; + for( int i = 0; i < clips.Length; i++ ) + { + referenceNode.AddLinkTo( SearchObject( clips[i] ) ); + + if( objectsToSearchSet.Contains( clips[i] ) ) + { + searchParameters.searchRefactoring( new AnimationSystemMatch( component, clips[i], ( newValue ) => + { + clips[i] = (AnimationClip) newValue; + modifiedClips = true; + } ) ); + } + } + + if( modifiedClips ) + AnimationUtility.SetAnimationClips( (Animation) component, clips ); + } + + // Search the objects that are animated by this Animation component for references + SearchAnimatedObjects( referenceNode ); + } + else if( component is Animator ) + { + // Search animation clips for references (via AnimatorController) + RuntimeAnimatorController animatorController = ( (Animator) component ).runtimeAnimatorController; + referenceNode.AddLinkTo( SearchObject( animatorController ) ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( animatorController ) ) + searchParameters.searchRefactoring( new AnimationSystemMatch( component, animatorController, ( newValue ) => ( (Animator) component ).runtimeAnimatorController = (RuntimeAnimatorController) newValue ) ); + + // Search the objects that are animated by this Animator component for references + SearchAnimatedObjects( referenceNode ); + } +#if UNITY_2017_2_OR_NEWER + else if( component is Tilemap ) + { + // Search the tiles for references + TileBase[] tiles = new TileBase[( (Tilemap) component ).GetUsedTilesCount()]; + ( (Tilemap) component ).GetUsedTilesNonAlloc( tiles ); + + if( tiles != null ) + { + for( int i = 0; i < tiles.Length; i++ ) + { + referenceNode.AddLinkTo( SearchObject( tiles[i] ), "Tile" ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( tiles[i] ) ) + searchParameters.searchRefactoring( new OtherSearchMatch( component, tiles[i], ( newValue ) => ( (Tilemap) component ).SwapTile( tiles[i], (TileBase) newValue ) ) ); + } + } + } +#endif +#if UNITY_2017_1_OR_NEWER + else if( component is PlayableDirector ) + { + // Search the PlayableAsset's scene bindings for references + PlayableAsset playableAsset = ( (PlayableDirector) component ).playableAsset; + if( playableAsset != null && !playableAsset.Equals( null ) ) + { + foreach( PlayableBinding binding in playableAsset.outputs ) + { + Object bindingValue = ( (PlayableDirector) component ).GetGenericBinding( binding.sourceObject ); + referenceNode.AddLinkTo( SearchObject( bindingValue ), "Binding: " + binding.streamName ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( bindingValue ) ) + searchParameters.searchRefactoring( new AnimationSystemMatch( component, bindingValue, ( newValue ) => ( (PlayableDirector) component ).SetGenericBinding( binding.sourceObject, newValue ) ) ); + } + } + } +#endif + else if( component is ParticleSystemRenderer ) + { + // Search ParticleSystemRenderer's custom meshes for references (at runtime, they can't be searched with reflection, unfortunately) + if( isInPlayMode && !AssetDatabase.Contains( component ) ) + { + Mesh[] meshes = new Mesh[( (ParticleSystemRenderer) component ).meshCount]; + int meshCount = ( (ParticleSystemRenderer) component ).GetMeshes( meshes ); + bool modifiedMeshes = false; + for( int i = 0; i < meshCount; i++ ) + { + referenceNode.AddLinkTo( SearchObject( meshes[i] ), "Renderer Module: Mesh" ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( meshes[i] ) ) + { + searchParameters.searchRefactoring( new OtherSearchMatch( component, meshes[i], ( newValue ) => + { + meshes[i] = (Mesh) newValue; + modifiedMeshes = true; + } ) ); + } + } + + if( modifiedMeshes ) + ( (ParticleSystemRenderer) component ).SetMeshes( meshes, meshCount ); + } + } + else if( component is ParticleSystem ) + { + // At runtime, some ParticleSystem properties can't be searched with reflection, search them manually here + if( isInPlayMode && !AssetDatabase.Contains( component ) ) + { + ParticleSystem particleSystem = (ParticleSystem) component; + + try + { + ParticleSystem.CollisionModule collisionModule = particleSystem.collision; +#if UNITY_2020_2_OR_NEWER + for( int i = 0, j = collisionModule.planeCount; i < j; i++ ) +#else + for( int i = 0, j = collisionModule.maxPlaneCount; i < j; i++ ) +#endif + { + Transform plane = collisionModule.GetPlane( i ); + referenceNode.AddLinkTo( SearchObject( plane ), "Collision Module: Plane" ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( plane ) ) + searchParameters.searchRefactoring( new OtherSearchMatch( collisionModule, plane, component, ( newValue ) => collisionModule.SetPlane( i, (Transform) newValue ) ) ); + } + } + catch { } + + try + { + ParticleSystem.TriggerModule triggerModule = particleSystem.trigger; +#if UNITY_2020_2_OR_NEWER + for( int i = 0, j = triggerModule.colliderCount; i < j; i++ ) +#else + for( int i = 0, j = triggerModule.maxColliderCount; i < j; i++ ) +#endif + { + Component collider = triggerModule.GetCollider( i ); + referenceNode.AddLinkTo( SearchObject( collider ), "Trigger Module: Collider" ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( collider ) ) + searchParameters.searchRefactoring( new OtherSearchMatch( triggerModule, collider, component, ( newValue ) => triggerModule.SetCollider( i, (Component) newValue ) ) ); + } + } + catch { } + +#if UNITY_2017_1_OR_NEWER + try + { + ParticleSystem.TextureSheetAnimationModule textureSheetAnimationModule = particleSystem.textureSheetAnimation; + for( int i = 0, j = textureSheetAnimationModule.spriteCount; i < j; i++ ) + { + Sprite sprite = textureSheetAnimationModule.GetSprite( i ); + referenceNode.AddLinkTo( SearchObject( sprite ), "Texture Sheet Animation Module: Sprite" ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( sprite ) ) + searchParameters.searchRefactoring( new OtherSearchMatch( textureSheetAnimationModule, sprite, component, ( newValue ) => textureSheetAnimationModule.SetSprite( i, (Sprite) newValue ) ) ); + } + } + catch { } +#endif + +#if UNITY_5_5_OR_NEWER + try + { + ParticleSystem.SubEmittersModule subEmittersModule = particleSystem.subEmitters; + for( int i = 0, j = subEmittersModule.subEmittersCount; i < j; i++ ) + { + ParticleSystem subEmitterSystem = subEmittersModule.GetSubEmitterSystem( i ); + referenceNode.AddLinkTo( SearchObject( subEmitterSystem ), "Sub Emitters Module: ParticleSystem" ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( subEmitterSystem ) ) + searchParameters.searchRefactoring( new OtherSearchMatch( subEmittersModule, subEmitterSystem, component, ( newValue ) => subEmittersModule.SetSubEmitterSystem( i, (ParticleSystem) newValue ) ) ); + } + } + catch { } +#endif + } + } + + SearchVariablesWithSerializedObject( referenceNode ); + return referenceNode; + } + + private ReferenceNode SearchMaterial( object obj ) + { + const string TEXTURE_PROPERTY_PREFIX = "m_SavedProperties.m_TexEnvs["; + + Material material = (Material) obj; + ReferenceNode referenceNode = PopReferenceNode( material ); + + // We used to search only the shader and the Texture properties in this function but it has changed for 2 major reasons: + // 1) Materials can store more than these references now. For example, HDRP materials can have references to other HDRP materials + // 2) It wasn't possible to search Texture properties that were no longer used by the shader + // Thus, we are searching every property of the material using SerializedObject + SearchVariablesWithSerializedObject( referenceNode ); + + // Post-process the found results and convert links that start with TEXTURE_PROPERTY_PREFIX to their readable names + SerializedObject materialSO = null; + for( int i = referenceNode.NumberOfOutgoingLinks - 1; i >= 0; i-- ) + { + List linkDescriptions = referenceNode[i].descriptions; + for( int j = linkDescriptions.Count - 1; j >= 0; j-- ) + { + int texturePropertyPrefixIndex = linkDescriptions[j].IndexOf( TEXTURE_PROPERTY_PREFIX ); + if( texturePropertyPrefixIndex >= 0 ) + { + texturePropertyPrefixIndex += TEXTURE_PROPERTY_PREFIX.Length; + int texturePropertyEndIndex = linkDescriptions[j].IndexOf( ']', texturePropertyPrefixIndex ); + if( texturePropertyEndIndex > texturePropertyPrefixIndex ) + { + int texturePropertyIndex; + if( int.TryParse( linkDescriptions[j].Substring( texturePropertyPrefixIndex, texturePropertyEndIndex - texturePropertyPrefixIndex ), out texturePropertyIndex ) ) + { + if( materialSO == null ) + materialSO = new SerializedObject( material ); + + string propertyName = materialSO.FindProperty( "m_SavedProperties.m_TexEnvs.Array.data[" + texturePropertyIndex + "].first" ).stringValue; + if( material.HasProperty( propertyName ) ) + linkDescriptions[j] = "[Property: " + propertyName + "]"; + else if( searchParameters.searchUnusedMaterialProperties ) + { + // Move unused references to the end of the list so that used references come first + linkDescriptions.Add( "[Property (UNUSED): " + propertyName + "]" ); + linkDescriptions.RemoveAt( j ); + } + else + linkDescriptions.RemoveAt( j ); + } + } + } + } + + if( linkDescriptions.Count == 0 ) // All shader properties were unused and we weren't searching for unused material properties + referenceNode.RemoveLink( i ); + } + + // At runtime, Textures assigned to clone materials can't be searched with reflection, search them manually here + if( searchTextureReferences && isInPlayMode && !AssetDatabase.Contains( material ) ) + { + Shader shader = material.shader; + int shaderPropertyCount = ShaderUtil.GetPropertyCount( shader ); + for( int i = 0; i < shaderPropertyCount; i++ ) + { + if( ShaderUtil.GetPropertyType( shader, i ) == ShaderUtil.ShaderPropertyType.TexEnv ) + { + string propertyName = ShaderUtil.GetPropertyName( shader, i ); + Texture assignedTexture = material.GetTexture( propertyName ); + if( objectsToSearchSet.Contains( assignedTexture ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( assignedTexture ), "Shader property: " + propertyName ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new OtherSearchMatch( material, assignedTexture, ( newValue ) => material.SetTexture( propertyName, (Texture) newValue ) ) ); + } + } + } + } + + return referenceNode; + } + + // Searches default Texture values assigned to shader properties, as well as #include references in shader source code + private ReferenceNode SearchShader( object obj ) + { + Shader shader = (Shader) obj; + ReferenceNode referenceNode = PopReferenceNode( shader ); + + if( searchTextureReferences ) + { + ShaderImporter shaderImporter = AssetImporter.GetAtPath( AssetDatabase.GetAssetPath( shader ) ) as ShaderImporter; + if( shaderImporter != null ) + { + int shaderPropertyCount = ShaderUtil.GetPropertyCount( shader ); + for( int i = 0; i < shaderPropertyCount; i++ ) + { + if( ShaderUtil.GetPropertyType( shader, i ) == ShaderUtil.ShaderPropertyType.TexEnv ) + { + string propertyName = ShaderUtil.GetPropertyName( shader, i ); + Texture defaultTexture = shaderImporter.GetDefaultTexture( propertyName ); +#if UNITY_2018_1_OR_NEWER + if( !defaultTexture ) + defaultTexture = shaderImporter.GetNonModifiableTexture( propertyName ); +#endif + + if( objectsToSearchSet.Contains( defaultTexture ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( defaultTexture ), "Default Texture: " + propertyName ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new AssetImporterDefaultValueMatch( shaderImporter, defaultTexture, propertyName, null ) ); + } + } + } + } + } + + // Search shader source code for #include references + if( shaderIncludesToSearchSet.Count > 0 ) + SearchShaderSourceCodeForCGIncludes( referenceNode ); + + return referenceNode; + } + + // Searches .compute, .cginc, .cg, .hlsl and .glslinc assets for #include references + private ReferenceNode SearchShaderSecondaryAsset( object obj ) + { + if( shaderIncludesToSearchSet.Count == 0 ) + return null; + + ReferenceNode referenceNode = PopReferenceNode( obj ); + SearchShaderSourceCodeForCGIncludes( referenceNode ); + return referenceNode; + } + + // Searches class/interface inheritances and default UnityEngine.Object values assigned to script variables + private ReferenceNode SearchMonoScript( object obj ) + { + MonoScript script = (MonoScript) obj; + Type scriptType = script.GetClass(); + if( scriptType == null || ( !scriptType.IsSubclassOf( typeof( MonoBehaviour ) ) && !scriptType.IsSubclassOf( typeof( ScriptableObject ) ) ) ) + return null; + + ReferenceNode referenceNode = PopReferenceNode( script ); + + // Check for class/interface inheritance references + for( int i = monoScriptsToSearch.Count - 1; i >= 0; i-- ) + { + if( monoScriptsToSearchTypes[i] != scriptType && monoScriptsToSearchTypes[i].IsAssignableFrom( scriptType ) ) + referenceNode.AddLinkTo( GetReferenceNode( monoScriptsToSearch[i] ), monoScriptsToSearchTypes[i].IsInterface ? "Implements interface" : "Extends class" ); + } + + MonoImporter scriptImporter = AssetImporter.GetAtPath( AssetDatabase.GetAssetPath( script ) ) as MonoImporter; + if( scriptImporter != null ) + { + VariableGetterHolder[] variables = GetFilteredVariablesForType( scriptType ); + for( int i = 0; i < variables.Length; i++ ) + { + if( variables[i].isSerializable && !variables[i].IsProperty ) + { + Object defaultValue = scriptImporter.GetDefaultReference( variables[i].Name ); + if( objectsToSearchSet.Contains( defaultValue ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( defaultValue ), "Default variable value: " + variables[i].Name ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new AssetImporterDefaultValueMatch( scriptImporter, defaultValue, variables[i].Name, variables ) ); + } + } + } + } + + return referenceNode; + } + + private ReferenceNode SearchAnimatorController( object obj ) + { + RuntimeAnimatorController controller = (RuntimeAnimatorController) obj; + ReferenceNode referenceNode = PopReferenceNode( controller ); + + if( controller is AnimatorController ) + { + AnimatorControllerLayer[] layers = ( (AnimatorController) controller ).layers; + for( int i = 0; i < layers.Length; i++ ) + { + if( objectsToSearchSet.Contains( layers[i].avatarMask ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( layers[i].avatarMask ), layers[i].name + " Mask" ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new AnimationSystemMatch( layers[i], layers[i].avatarMask, controller, ( newValue ) => layers[i].avatarMask = (AvatarMask) newValue ) ); + } + + referenceNode.AddLinkTo( SearchObject( layers[i].stateMachine ) ); + } + } + else + { + if( controller is AnimatorOverrideController ) + { + RuntimeAnimatorController parentController = ( (AnimatorOverrideController) controller ).runtimeAnimatorController; + if( objectsToSearchSet.Contains( parentController ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( parentController ) ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new AnimationSystemMatch( controller, parentController, ( newValue ) => ( (AnimatorOverrideController) controller ).runtimeAnimatorController = (RuntimeAnimatorController) newValue ) ); + } + + if( searchParameters.searchRefactoring != null ) + { + List> overrideClips = new List>( ( (AnimatorOverrideController) controller ).overridesCount ); + ( (AnimatorOverrideController) controller ).GetOverrides( overrideClips ); + bool modifiedOverrideClips = false; + for( int i = overrideClips.Count - 1; i >= 0; i-- ) + { + if( objectsToSearchSet.Contains( overrideClips[i].Value ) ) + { + searchParameters.searchRefactoring( new AnimationSystemMatch( controller, overrideClips[i].Value, ( newValue ) => + { + overrideClips[i] = new KeyValuePair( overrideClips[i].Key, (AnimationClip) newValue ); + modifiedOverrideClips = true; + } ) ); + } + } + + if( modifiedOverrideClips ) + ( (AnimatorOverrideController) controller ).ApplyOverrides( overrideClips ); + } + } + + AnimationClip[] animClips = controller.animationClips; + for( int i = 0; i < animClips.Length; i++ ) + referenceNode.AddLinkTo( SearchObject( animClips[i] ) ); + } + + return referenceNode; + } + + private ReferenceNode SearchAnimatorStateMachine( object obj ) + { + AnimatorStateMachine animatorStateMachine = (AnimatorStateMachine) obj; + ReferenceNode referenceNode = PopReferenceNode( animatorStateMachine ); + + ChildAnimatorStateMachine[] stateMachines = animatorStateMachine.stateMachines; + for( int i = 0; i < stateMachines.Length; i++ ) + referenceNode.AddLinkTo( SearchObject( stateMachines[i].stateMachine ), "Child State Machine" ); + + ChildAnimatorState[] states = animatorStateMachine.states; + for( int i = 0; i < states.Length; i++ ) + referenceNode.AddLinkTo( SearchObject( states[i].state ) ); + + if( searchMonoBehavioursForScript ) + { + StateMachineBehaviour[] behaviours = animatorStateMachine.behaviours; + for( int i = 0; i < behaviours.Length; i++ ) + { + MonoScript script = MonoScript.FromScriptableObject( behaviours[i] ); + if( objectsToSearchSet.Contains( script ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( script ) ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new BehaviourUsageMatch( animatorStateMachine, script, behaviours[i] ) ); + } + } + } + + return referenceNode; + } + + private ReferenceNode SearchAnimatorState( object obj ) + { + AnimatorState animatorState = (AnimatorState) obj; + ReferenceNode referenceNode = PopReferenceNode( animatorState ); + + referenceNode.AddLinkTo( SearchObject( animatorState.motion ), "Motion" ); + + if( searchParameters.searchRefactoring != null && animatorState.motion as AnimationClip && objectsToSearchSet.Contains( animatorState.motion ) ) + searchParameters.searchRefactoring( new AnimationSystemMatch( animatorState, animatorState.motion, ( newValue ) => animatorState.motion = (Motion) newValue ) ); + + if( searchMonoBehavioursForScript ) + { + StateMachineBehaviour[] behaviours = animatorState.behaviours; + for( int i = 0; i < behaviours.Length; i++ ) + { + MonoScript script = MonoScript.FromScriptableObject( behaviours[i] ); + if( objectsToSearchSet.Contains( script ) ) + { + referenceNode.AddLinkTo( GetReferenceNode( script ) ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new BehaviourUsageMatch( animatorState, script, behaviours[i] ) ); + } + } + } + + return referenceNode; + } + + private ReferenceNode SearchAnimatorStateTransition( object obj ) + { + // Don't search AnimatorStateTransition objects, it will just return duplicate results of SearchAnimatorStateMachine + return PopReferenceNode( obj ); + } + + private ReferenceNode SearchBlendTree( object obj ) + { + BlendTree blendTree = (BlendTree) obj; + ReferenceNode referenceNode = PopReferenceNode( blendTree ); + + ChildMotion[] children = blendTree.children; + for( int i = 0; i < children.Length; i++ ) + { + referenceNode.AddLinkTo( SearchObject( children[i].motion ), "Motion" ); + + if( searchParameters.searchRefactoring != null && children[i].motion as AnimationClip && objectsToSearchSet.Contains( children[i].motion ) ) + searchParameters.searchRefactoring( new AnimationSystemMatch( blendTree, children[i].motion, ( newValue ) => children[i].motion = (Motion) newValue ) ); + } + + return referenceNode; + } + + private ReferenceNode SearchAnimationClip( object obj ) + { + AnimationClip clip = (AnimationClip) obj; + ReferenceNode referenceNode = PopReferenceNode( clip ); + + // Get all curves from animation clip + EditorCurveBinding[] objectCurves = AnimationUtility.GetObjectReferenceCurveBindings( clip ); + for( int i = 0; i < objectCurves.Length; i++ ) + { + // Search through all the keyframes in this curve + ObjectReferenceKeyframe[] keyframes = AnimationUtility.GetObjectReferenceCurve( clip, objectCurves[i] ); + bool modifiedKeyframes = false; + for( int j = 0; j < keyframes.Length; j++ ) + { + referenceNode.AddLinkTo( SearchObject( keyframes[j].value ), "Keyframe: " + keyframes[j].time ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( keyframes[j].value ) ) + { + searchParameters.searchRefactoring( new AnimationSystemMatch( clip, keyframes[j].value, ( newValue ) => + { + keyframes[j].value = newValue; + modifiedKeyframes = true; + } ) ); + } + } + + if( modifiedKeyframes ) + AnimationUtility.SetObjectReferenceCurve( clip, objectCurves[i], keyframes ); + } + + // Get all events from animation clip + AnimationEvent[] events = AnimationUtility.GetAnimationEvents( clip ); + bool modifiedEvents = false; + for( int i = 0; i < events.Length; i++ ) + { + referenceNode.AddLinkTo( SearchObject( events[i].objectReferenceParameter ), "AnimationEvent: " + events[i].time ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( events[i].objectReferenceParameter ) ) + { + searchParameters.searchRefactoring( new AnimationSystemMatch( clip, events[i].objectReferenceParameter, ( newValue ) => + { + events[i].objectReferenceParameter = newValue; + modifiedEvents = true; + } ) ); + } + } + + if( modifiedEvents ) + AnimationUtility.SetAnimationEvents( clip, events ); + + return referenceNode; + } + + // TerrainData's properties like tree/detail/layer definitions aren't exposed to SerializedObject so use reflection instead + private ReferenceNode SearchTerrainData( object obj ) + { + ReferenceNode referenceNode = PopReferenceNode( obj ); + SearchVariablesWithReflection( referenceNode ); + return referenceNode; + } + + private ReferenceNode SearchLightmapSettings( object obj ) + { + ReferenceNode referenceNode = PopReferenceNode( obj ); + + referenceNode.AddLinkTo( SearchObject( LightmapSettings.lightProbes ), "Light Probes" ); + + LightmapData[] lightmaps = LightmapSettings.lightmaps; + if( lightmaps != null ) + { + for( int i = 0; i < lightmaps.Length; i++ ) + referenceNode.AddLinkTo( SearchObject( lightmaps[i] ), "Lightmap" ); + } + + SearchVariablesWithSerializedObject( referenceNode, true ); + return referenceNode; + } + + private ReferenceNode SearchRenderSettings( object obj ) + { + ReferenceNode referenceNode = PopReferenceNode( obj ); + +#if UNITY_2021_2_OR_NEWER + referenceNode.AddLinkTo( SearchObject( defaultReflectionProbeGetter() ), "Default Reflection Probe" ); +#else + referenceNode.AddLinkTo( SearchObject( ReflectionProbe.defaultTexture ), "Default Reflection Probe" ); +#endif + SearchVariablesWithSerializedObject( referenceNode, true ); + return referenceNode; + } + +#if UNITY_2017_1_OR_NEWER + private ReferenceNode SearchSpriteAtlas( object obj ) + { + SpriteAtlas spriteAtlas = (SpriteAtlas) obj; + ReferenceNode referenceNode = PopReferenceNode( spriteAtlas ); + + SerializedObject spriteAtlasSO = new SerializedObject( spriteAtlas ); + if( spriteAtlas.isVariant ) + { + SerializedProperty masterAtlasProperty = spriteAtlasSO.FindProperty( "m_MasterAtlas" ); + Object masterAtlas = masterAtlasProperty.objectReferenceValue; + if( objectsToSearchSet.Contains( masterAtlas ) ) + { + referenceNode.AddLinkTo( SearchObject( masterAtlas ), "Master Atlas" ); + + if( searchParameters.searchRefactoring != null ) + searchParameters.searchRefactoring( new SerializedPropertyMatch( spriteAtlas, masterAtlas, masterAtlasProperty ) ); + } + } + + SerializedProperty packables = spriteAtlasSO.FindProperty( "m_EditorData.packables" ); + if( packables != null ) + { + for( int i = 0, length = packables.arraySize; i < length; i++ ) + { + SerializedProperty packedSpriteProperty = packables.GetArrayElementAtIndex( i ); + Object packedSprite = packedSpriteProperty.objectReferenceValue; + SearchSpriteAtlas( referenceNode, packedSprite ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( packedSprite ) ) + searchParameters.searchRefactoring( new SerializedPropertyMatch( spriteAtlas, packedSprite, packedSpriteProperty ) ); + } + } +#if UNITY_2018_2_OR_NEWER + else + { + Object[] _packables = spriteAtlas.GetPackables(); + if( _packables != null ) + { + for( int i = 0; i < _packables.Length; i++ ) + SearchSpriteAtlas( referenceNode, _packables[i] ); + } + } +#endif + + return referenceNode; + } + + private void SearchSpriteAtlas( ReferenceNode referenceNode, Object packedAsset ) + { + if( packedAsset == null || packedAsset.Equals( null ) ) + return; + + referenceNode.AddLinkTo( SearchObject( packedAsset ), "Packed Texture" ); + + if( packedAsset is Texture ) + { + // Search the Texture's sprites if the Texture asset isn't included in the "SEARCHED OBJECTS" list (i.e. user has + // added only a Sprite sub-asset of the Texture to the list, not the Texture asset itself). Otherwise, references to + // both the Texture and its sprites will be found which can be considered as duplicate references + if( AssetDatabase.IsMainAsset( packedAsset ) && !assetsToSearchSet.Contains( packedAsset ) ) + { + Object[] textureSubAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath( AssetDatabase.GetAssetPath( packedAsset ) ); + for( int i = 0; i < textureSubAssets.Length; i++ ) + { + if( textureSubAssets[i] is Sprite ) + referenceNode.AddLinkTo( SearchObject( textureSubAssets[i] ), "Packed Texture" ); + } + } + } + else if( packedAsset.IsFolder() ) + { + // Search all Sprites in the folder + string[] texturesInFolder = AssetDatabase.FindAssets( "t:Texture2D", new string[] { AssetDatabase.GetAssetPath( packedAsset ) } ); + if( texturesInFolder != null ) + { + for( int i = 0; i < texturesInFolder.Length; i++ ) + { + string texturePath = AssetDatabase.GUIDToAssetPath( texturesInFolder[i] ); + TextureImporter textureImporter = AssetImporter.GetAtPath( texturePath ) as TextureImporter; + if( textureImporter != null && textureImporter.textureType == TextureImporterType.Sprite ) + { + // Search the Texture and its sprites + SearchSpriteAtlas( referenceNode, AssetDatabase.LoadMainAssetAtPath( texturePath ) ); + } + } + } + } + } +#endif + +#if UNITY_2017_3_OR_NEWER + // Find references from an Assembly Definition File to its Assembly Definition References + private ReferenceNode SearchAssemblyDefinitionFile( object obj ) + { + if( assemblyDefinitionFilesToSearch.Count == 0 ) + return null; + + AssemblyDefinitionReferences assemblyDefinitionFile = JsonUtility.FromJson( ( (TextAsset) obj ).text ); + ReferenceNode referenceNode = PopReferenceNode( obj ); + + if( !string.IsNullOrEmpty( assemblyDefinitionFile.reference ) ) + { + if( assemblyDefinitionFile.references == null ) + assemblyDefinitionFile.references = new List( 1 ) { assemblyDefinitionFile.reference }; + else + assemblyDefinitionFile.references.Add( assemblyDefinitionFile.reference ); + } + + if( assemblyDefinitionFile.references != null ) + { + for( int i = 0; i < assemblyDefinitionFile.references.Count; i++ ) + { +#if UNITY_2019_1_OR_NEWER + string assemblyPath = CompilationPipeline.GetAssemblyDefinitionFilePathFromAssemblyReference( assemblyDefinitionFile.references[i] ); +#else + string assemblyPath = CompilationPipeline.GetAssemblyDefinitionFilePathFromAssemblyName( assemblyDefinitionFile.references[i] ); +#endif + if( !string.IsNullOrEmpty( assemblyPath ) ) + { + Object searchedAssemblyDefinitionFile; + if( assemblyDefinitionFilesToSearch.TryGetValue( assemblyPath, out searchedAssemblyDefinitionFile ) ) + referenceNode.AddLinkTo( GetReferenceNode( searchedAssemblyDefinitionFile ), "Referenced Assembly" ); + } + } + } + + return referenceNode; + } +#endif + +#if UNITY_2018_1_OR_NEWER + // Searches Shader Graph assets for references + private ReferenceNode SearchShaderGraph( object obj ) + { + if( !searchTextureReferences && !searchShaderGraphsForSubGraphs && shaderIncludesToSearchSet.Count == 0 ) + return null; + + ReferenceNode referenceNode = PopReferenceNode( obj ); + + // Shader Graph assets are JSON files, they must be crawled manually to find references + string graphJson = File.ReadAllText( AssetDatabase.GetAssetPath( (Object) obj ) ); + if( graphJson.IndexOf( "\"m_ObjectId\"", 0, Mathf.Min( 200, graphJson.Length ) ) >= 0 ) + { + // New Shader Graph serialization format is used: https://github.com/Unity-Technologies/Graphics/pull/222 + // Iterate over all these occurrences: "guid\": \"GUID_VALUE\" (\" is used instead of " because it is a nested JSON) + IterateOverValuesInString( graphJson, new string[] { "\"guid\\\"" }, '"', ( guid ) => + { + if( guid.Length > 1 ) + { + if( guid[guid.Length - 1] == '\\' ) + guid = guid.Substring( 0, guid.Length - 1 ); + + string referencePath = AssetDatabase.GUIDToAssetPath( guid ); + if( !string.IsNullOrEmpty( referencePath ) && assetsToSearchPathsSet.Contains( referencePath ) ) + { + Object reference = AssetDatabase.LoadMainAssetAtPath( referencePath ); + if( objectsToSearchSet.Contains( reference ) ) + referenceNode.AddLinkTo( GetReferenceNode( reference ), "Used in graph" ); + } + } + } ); + + if( shaderIncludesToSearchSet.Count > 0 ) + { + // Iterate over all these occurrences: "m_FunctionSource": "GUID_VALUE" (this one is not nested JSON) + IterateOverValuesInString( graphJson, new string[] { "\"m_FunctionSource\"" }, '"', ( guid ) => + { + string referencePath = AssetDatabase.GUIDToAssetPath( guid ); + if( !string.IsNullOrEmpty( referencePath ) && assetsToSearchPathsSet.Contains( referencePath ) ) + { + Object reference = AssetDatabase.LoadMainAssetAtPath( referencePath ); + if( objectsToSearchSet.Contains( reference ) ) + referenceNode.AddLinkTo( GetReferenceNode( reference ), "Used in node: Custom Function" ); + } + } ); + } + } + else + { + // Old Shader Graph serialization format is used. Although we could use the same search method as the new serialization format (which + // is potentially faster), this alternative search method yields more information about references + ShaderGraphReferences shaderGraph = JsonUtility.FromJson( graphJson ); + + if( shaderGraph.m_SerializedProperties != null ) + { + for( int i = shaderGraph.m_SerializedProperties.Count - 1; i >= 0; i-- ) + { + string propertyJSON = shaderGraph.m_SerializedProperties[i].JSONnodeData; + if( string.IsNullOrEmpty( propertyJSON ) ) + continue; + + ShaderGraphReferences.PropertyData propertyData = JsonUtility.FromJson( propertyJSON ); + if( propertyData.m_Value == null ) + continue; + + string texturePath = propertyData.m_Value.GetTexturePath(); + if( string.IsNullOrEmpty( texturePath ) || !assetsToSearchPathsSet.Contains( texturePath ) ) + continue; + + Texture texture = AssetDatabase.LoadAssetAtPath( texturePath ); + if( objectsToSearchSet.Contains( texture ) ) + referenceNode.AddLinkTo( GetReferenceNode( texture ), "Default Texture: " + propertyData.GetName() ); + } + } + + if( shaderGraph.m_SerializableNodes != null ) + { + for( int i = shaderGraph.m_SerializableNodes.Count - 1; i >= 0; i-- ) + { + string nodeJSON = shaderGraph.m_SerializableNodes[i].JSONnodeData; + if( string.IsNullOrEmpty( nodeJSON ) ) + continue; + + ShaderGraphReferences.NodeData nodeData = JsonUtility.FromJson( nodeJSON ); + if( !string.IsNullOrEmpty( nodeData.m_FunctionSource ) ) + { + string customFunctionPath = AssetDatabase.GUIDToAssetPath( nodeData.m_FunctionSource ); + if( !string.IsNullOrEmpty( customFunctionPath ) && assetsToSearchPathsSet.Contains( customFunctionPath ) ) + { + Object customFunction = AssetDatabase.LoadMainAssetAtPath( customFunctionPath ); + if( objectsToSearchSet.Contains( customFunction ) ) + referenceNode.AddLinkTo( GetReferenceNode( customFunction ), "Used in node: " + nodeData.m_Name ); + } + } + + if( searchShaderGraphsForSubGraphs ) + { + string subGraphPath = nodeData.GetSubGraphPath(); + if( !string.IsNullOrEmpty( subGraphPath ) && assetsToSearchPathsSet.Contains( subGraphPath ) ) + { + Object subGraph = AssetDatabase.LoadMainAssetAtPath( subGraphPath ); + if( objectsToSearchSet.Contains( subGraph ) ) + referenceNode.AddLinkTo( GetReferenceNode( subGraph ), "Used as Sub-graph" ); + } + } + + if( nodeData.m_SerializableSlots == null ) + continue; + + for( int j = nodeData.m_SerializableSlots.Count - 1; j >= 0; j-- ) + { + string nodeSlotJSON = nodeData.m_SerializableSlots[j].JSONnodeData; + if( string.IsNullOrEmpty( nodeSlotJSON ) ) + continue; + + string texturePath = JsonUtility.FromJson( nodeSlotJSON ).GetTexturePath(); + if( string.IsNullOrEmpty( texturePath ) || !assetsToSearchPathsSet.Contains( texturePath ) ) + continue; + + Texture texture = AssetDatabase.LoadAssetAtPath( texturePath ); + if( objectsToSearchSet.Contains( texture ) ) + referenceNode.AddLinkTo( GetReferenceNode( texture ), "Used in node: " + nodeData.m_Name ); + } + } + } + } + + return referenceNode; + } +#endif + +#if ASSET_USAGE_VFX_GRAPH + private ReferenceNode SearchVFXGraphAsset( object obj ) + { + ReferenceNode referenceNode = PopReferenceNode( obj ); + + object vfxResource = vfxResourceGetter( AssetDatabase.GetAssetPath( (Object) obj ) ); + foreach( Object vfxResourceContent in (Object[]) vfxResourceContentsGetter.Invoke( vfxResource, null ) ) + referenceNode.AddLinkTo( SearchObject( vfxResourceContent ) ); + + return referenceNode; + } +#endif + + // Find references from an Animation/Animator component to the objects that it animates + private void SearchAnimatedObjects( ReferenceNode referenceNode ) + { + GameObject root = ( (Component) referenceNode.nodeObject ).gameObject; + AnimationClip[] clips = AnimationUtility.GetAnimationClips( root ); + for( int i = 0; i < clips.Length; i++ ) + { + AnimationClip clip = clips[i]; + if( !clip ) + continue; + + bool isClipUnique = true; + for( int j = i - 1; j >= 0; j-- ) + { + if( clips[j] == clip ) + { + isClipUnique = false; + break; + } + } + + if( !isClipUnique ) + continue; + + EditorCurveBinding[] uniqueBindings; + if( !animationClipUniqueBindings.TryGetValue( clip, out uniqueBindings ) ) + { + // Calculate all the "unique" paths that the animation clip's curves have + // Both float curves (GetCurveBindings) and object reference curves (GetObjectReferenceCurveBindings) are checked + List _uniqueBindings = new List( 2 ); + EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings( clip ); + for( int j = 0; j < bindings.Length; j++ ) + { + string bindingPath = bindings[j].path; + if( string.IsNullOrEmpty( bindingPath ) ) // Ignore the root animated object + continue; + + bool isBindingUnique = true; + for( int k = _uniqueBindings.Count - 1; k >= 0; k-- ) + { + if( bindingPath == _uniqueBindings[k].path ) + { + isBindingUnique = false; + break; + } + } + + if( isBindingUnique ) + _uniqueBindings.Add( bindings[j] ); + } + + bindings = AnimationUtility.GetObjectReferenceCurveBindings( clip ); + for( int j = 0; j < bindings.Length; j++ ) + { + string bindingPath = bindings[j].path; + if( string.IsNullOrEmpty( bindingPath ) ) // Ignore the root animated object + continue; + + bool isBindingUnique = true; + for( int k = _uniqueBindings.Count - 1; k >= 0; k-- ) + { + if( bindingPath == _uniqueBindings[k].path ) + { + isBindingUnique = false; + break; + } + } + + if( isBindingUnique ) + _uniqueBindings.Add( bindings[j] ); + } + + uniqueBindings = _uniqueBindings.ToArray(); + animationClipUniqueBindings[clip] = uniqueBindings; + } + + string clipName = clip.name; + for( int j = 0; j < uniqueBindings.Length; j++ ) + referenceNode.AddLinkTo( SearchObject( AnimationUtility.GetAnimatedObject( root, uniqueBindings[j] ) ), "Animated via clip: " + clipName ); + } + } + + // Search #include references in shader source code + private void SearchShaderSourceCodeForCGIncludes( ReferenceNode referenceNode ) + { + string shaderPath = AssetDatabase.GetAssetPath( (Object) referenceNode.nodeObject ); + + // Iterate over all these occurrences: #include "INCLUDE_REFERENCE" or #include_with_pragmas "INCLUDE_REFERENCE" + IterateOverValuesInString( File.ReadAllText( shaderPath ), new string[] { "#include ", "#include_with_pragmas " }, '"', ( include ) => + { + bool isIncludePotentialReference = shaderIncludesToSearchSet.Contains( include ); + if( !isIncludePotentialReference ) + { + // Get absolute path of the #include + include = Path.GetFullPath( Path.Combine( Path.GetDirectoryName( shaderPath ), include ) ); + + int trimStartLength = Directory.GetCurrentDirectory().Length + 1; // Convert absolute path to a Project-relative path + if( include.Length > trimStartLength ) + { + include = include.Substring( trimStartLength ).Replace( '\\', '/' ); + isIncludePotentialReference = shaderIncludesToSearchSet.Contains( include ); + } + } + + if( isIncludePotentialReference ) + { + Object cgShader = AssetDatabase.LoadMainAssetAtPath( include ); + if( objectsToSearchSet.Contains( cgShader ) ) + referenceNode.AddLinkTo( GetReferenceNode( cgShader ), "Used with #include" ); + } + } ); + } + + // Search through variables of an object with SerializedObject + private void SearchVariablesWithSerializedObject( ReferenceNode referenceNode, bool forceUseSerializedObject = false ) + { + Object unityObject = (Object) referenceNode.nodeObject; + if( !isInPlayMode || unityObject.IsAsset() || forceUseSerializedObject ) + { +#if ASSET_USAGE_ADDRESSABLES + // See: https://github.com/yasirkula/UnityAssetUsageDetector/issues/29 + if( searchParameters.addressablesSupport && unityObject.name == "Deprecated EditorExtensionImpl" ) + return; +#endif + + SerializedObject so = new SerializedObject( unityObject ); + SerializedProperty iterator = so.GetIterator(); + SerializedProperty iteratorVisible = so.GetIterator(); + if( iterator.Next( true ) ) + { + bool iteratingVisible = iteratorVisible.NextVisible( true ); +#if UNITY_2018_3_OR_NEWER + bool searchPrefabOverridesOnly = searchParameters.hideReduntantPrefabVariantLinks && unityObject.IsAsset() && PrefabUtility.GetCorrespondingObjectFromSource( unityObject ) != null; +#endif + bool enterChildren; + do + { + // Iterate over NextVisible properties AND the properties that have corresponding FieldInfos (internal Unity + // properties don't have FieldInfos so we are skipping them, which is good because search results found in + // those properties aren't interesting and mostly confusing) + bool shouldMoveVisibleIterator = iteratingVisible && SerializedProperty.EqualContents( iterator, iteratorVisible ); + bool isVisible = shouldMoveVisibleIterator || iterator.type == "Array"; + if( !isVisible ) + { + Type propFieldType; + isVisible = fieldInfoGetter( iterator, out propFieldType ) != null; + } + + if( !isVisible ) + enterChildren = false; +#if UNITY_2018_3_OR_NEWER + else if( searchPrefabOverridesOnly && !iterator.prefabOverride ) + enterChildren = false; +#endif + else + { + Object propertyValue; + ReferenceNode searchResult; + switch( iterator.propertyType ) + { + case SerializedPropertyType.ObjectReference: + propertyValue = iterator.objectReferenceValue; + searchResult = SearchObject( PreferablyGameObject( propertyValue ) ); + enterChildren = false; + break; + case SerializedPropertyType.ExposedReference: + propertyValue = iterator.exposedReferenceValue; + searchResult = SearchObject( PreferablyGameObject( propertyValue ) ); + enterChildren = false; + break; +#if UNITY_2019_3_OR_NEWER + case SerializedPropertyType.ManagedReference: + object managedReferenceValue = GetRawSerializedPropertyValue( iterator ); + propertyValue = managedReferenceValue as Object; + searchResult = SearchObject( PreferablyGameObject( managedReferenceValue ) ); + enterChildren = false; + break; +#endif + case SerializedPropertyType.Generic: +#if ASSET_USAGE_ADDRESSABLES + if( searchParameters.addressablesSupport && iterator.type.StartsWithFast( "AssetReference" ) && GetRawSerializedPropertyValue( iterator ) is AssetReference assetReference ) + { + propertyValue = GetAddressablesAssetReferenceValue( assetReference ); + searchResult = SearchObject( PreferablyGameObject( propertyValue ) ); + enterChildren = false; + } + else +#endif +#if ASSET_USAGE_VFX_GRAPH + if( vfxSerializableObjectValueGetter != null && iterator.type == "VFXSerializableObject" && GetRawSerializedPropertyValue( iterator ) is object vfxSerializableObject ) + { + object vfxSerializableObjectValue = vfxSerializableObjectValueGetter.Invoke( vfxSerializableObject, null ); + propertyValue = vfxSerializableObjectValue as Object; + searchResult = SearchObject( PreferablyGameObject( vfxSerializableObjectValue ) ); + enterChildren = false; + } + else +#endif + { + propertyValue = null; + searchResult = null; + enterChildren = true; + } + + break; + default: + propertyValue = null; + searchResult = null; + enterChildren = false; + break; + } + + if( searchResult != null && searchResult != referenceNode ) + { + string propertyPath = iterator.propertyPath; + + // m_RD.texture is a redundant reference that shows up when searching sprites + if( !propertyPath.EndsWithFast( "m_RD.texture" ) ) + { + referenceNode.AddLinkTo( searchResult, "Variable: " + propertyPath.Replace( ".Array.data[", "[" ) ); // "arrayVariable.Array.data[0]" becomes "arrayVariable[0]" + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( propertyValue ) ) + searchParameters.searchRefactoring( new SerializedPropertyMatch( unityObject, propertyValue, iterator ) ); + } + } + } + + if( shouldMoveVisibleIterator ) + iteratingVisible = iteratorVisible.NextVisible( enterChildren ); + } while( iterator.Next( enterChildren ) ); + + return; + } + } + + // Use reflection algorithm as fallback + SearchVariablesWithReflection( referenceNode ); + } + + // Search through variables of an object with reflection + private void SearchVariablesWithReflection( ReferenceNode referenceNode ) + { + // Get filtered variables for this object + VariableGetterHolder[] variables = GetFilteredVariablesForType( referenceNode.nodeObject.GetType() ); + for( int i = 0; i < variables.Length; i++ ) + { + // When possible, don't search non-serializable variables + if( searchSerializableVariablesOnly && !variables[i].isSerializable ) + continue; + + try + { + object variableValue = variables[i].Get( referenceNode.nodeObject ); + if( variableValue == null || variableValue.Equals( null ) ) + continue; + + // Values stored inside ICollection objects are searched using IEnumerable, + // no need to have duplicate search entries + if( !( variableValue is ICollection ) ) + { +#if ASSET_USAGE_ADDRESSABLES + if( searchParameters.addressablesSupport && variableValue is AssetReference ) + { + variableValue = GetAddressablesAssetReferenceValue( (AssetReference) variableValue ); + if( variableValue == null || variableValue.Equals( null ) ) + continue; + } +#endif + + ReferenceNode searchResult = SearchObject( PreferablyGameObject( variableValue ) ); + if( searchResult != null && searchResult != referenceNode ) + { + referenceNode.AddLinkTo( searchResult, ( variables[i].IsProperty ? "Property: " : "Variable: " ) + variables[i].Name ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( variableValue as Object ) ) + searchParameters.searchRefactoring( new ReflectionMatch( referenceNode.nodeObject, (Object) variableValue, variables[i].variable ) ); + } + } + + if( variableValue is IEnumerable && !( variableValue is Transform ) ) + { + // If the field is IEnumerable (possibly an array or collection), search through members of it + // Note that Transform IEnumerable (children of the transform) is not iterated + int index = 0; + List foundReferences = null; + foreach( object element in (IEnumerable) variableValue ) + { + ReferenceNode searchResult = SearchObject( PreferablyGameObject( element ) ); + if( searchResult != null && searchResult != referenceNode ) + { + referenceNode.AddLinkTo( searchResult, string.Concat( variables[i].IsProperty ? "Property: " : "Variable: ", variables[i].Name, "[", index + "]" ) ); + + if( searchParameters.searchRefactoring != null && objectsToSearchSet.Contains( element as Object ) ) + { + if( foundReferences == null ) + foundReferences = new List( 2 ) { (Object) element }; + else if( !foundReferences.Contains( (Object) element ) ) + foundReferences.Add( (Object) element ); + } + } + + index++; + } + + if( foundReferences != null ) + { + for( int j = foundReferences.Count - 1; j >= 0; j-- ) + searchParameters.searchRefactoring( new ReflectionMatch( referenceNode.nodeObject, foundReferences[j], variableValue ) ); + } + } + } + catch( UnassignedReferenceException ) { } + catch( MissingReferenceException ) { } + catch( MissingComponentException ) { } + catch( NotImplementedException ) { } + catch( Exception e ) + { + // Unknown exceptions usually occur when variableValue is an IEnumerable and its enumerator throws an unhandled exception in MoveNext or Current + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + sb.EnsureCapacity( callStack.Count * 50 + 1000 ); + + sb.Append( "Skipped searching " ).Append( referenceNode.nodeObject.GetType().FullName ).Append( "." ).Append( variables[i].Name ).AppendLine( " because it threw exception:" ).Append( e ).AppendLine(); + + Object latestUnityObjectInCallStack = AppendCallStackToStringBuilder( sb ); + Debug.LogWarning( sb.ToString(), latestUnityObjectInCallStack ); + } + } + } + + // Get filtered variables for a type + private VariableGetterHolder[] GetFilteredVariablesForType( Type type ) + { + VariableGetterHolder[] result; + if( typeToVariables.TryGetValue( type, out result ) ) + return result; + + // This is the first time this type of object is seen, filter and cache its variables + // Variable filtering process: + // 1- skip Obsolete variables + // 2- skip primitive types, enums and strings + // 3- skip common Unity types that can't hold any references (e.g. Vector3, Rect, Color, Quaternion) + // + // P.S. IsIgnoredUnityType() extension function handles steps 2) and 3) + + validVariables.Clear(); + + // Filter the fields + if( fieldModifiers != ( BindingFlags.Instance | BindingFlags.DeclaredOnly ) ) + { + Type currType = type; + while( currType != typeof( object ) ) + { + FieldInfo[] fields = currType.GetFields( fieldModifiers ); + for( int i = 0; i < fields.Length; i++ ) + { + FieldInfo field = fields[i]; + + // Skip obsolete fields + if( Attribute.IsDefined( field, typeof( ObsoleteAttribute ) ) ) + continue; + + // Skip primitive types + if( field.FieldType.IsIgnoredUnityType() ) + continue; + +#if UNITY_2021_2_OR_NEWER + // "ref struct"s can't be accessed via reflection + if( field.FieldType.IsByRefLike ) + continue; +#endif + + // Additional filtering for fields: + // 1- Ignore "m_RectTransform", "m_CanvasRenderer" and "m_Canvas" fields of Graphic components + string fieldName = field.Name; + if( typeof( Graphic ).IsAssignableFrom( currType ) && + ( fieldName == "m_RectTransform" || fieldName == "m_CanvasRenderer" || fieldName == "m_Canvas" ) ) + continue; + + VariableGetVal getter = field.CreateGetter( type ); + if( getter != null ) + validVariables.Add( new VariableGetterHolder( field, getter, searchSerializableVariablesOnly ? field.IsSerializable() : true ) ); + } + + currType = currType.BaseType; + } + } + + if( propertyModifiers != ( BindingFlags.Instance | BindingFlags.DeclaredOnly ) ) + { + Type currType = type; + while( currType != typeof( object ) ) + { + PropertyInfo[] properties = currType.GetProperties( propertyModifiers ); + for( int i = 0; i < properties.Length; i++ ) + { + PropertyInfo property = properties[i]; + + // Skip obsolete properties + if( Attribute.IsDefined( property, typeof( ObsoleteAttribute ) ) ) + continue; + + // Skip primitive types + if( property.PropertyType.IsIgnoredUnityType() ) + continue; + +#if UNITY_2021_2_OR_NEWER + // "ref struct"s can't be accessed via reflection + if( property.PropertyType.IsByRefLike ) + continue; +#endif + + // Skip properties without a getter function + MethodInfo propertyGetter = property.GetGetMethod( true ); + if( propertyGetter == null ) + continue; + + // Skip indexer properties + if( property.GetIndexParameters().Length > 0 ) + continue; + + // No need to check properties with 'override' keyword + if( propertyGetter.GetBaseDefinition().DeclaringType != propertyGetter.DeclaringType ) + continue; + + string propertyName = property.Name; + + // Ignore "gameObject", "transform", "rectTransform" and "attachedRigidbody" properties of components to get more useful results + if( typeof( Component ).IsAssignableFrom( currType ) && ( propertyName == "gameObject" || + propertyName == "transform" || propertyName == "attachedRigidbody" || propertyName == "rectTransform" ) ) + continue; + // Ignore "canvasRenderer" and "canvas" properties of Graphic components to get more useful results + else if( typeof( Graphic ).IsAssignableFrom( currType ) && + ( propertyName == "canvasRenderer" || propertyName == "canvas" ) ) + continue; + // Prevent accessing properties of Unity that instantiate an existing resource (causing memory leak) + else if( typeof( MeshFilter ).IsAssignableFrom( currType ) && propertyName == "mesh" ) + continue; + // Same as above + else if( ( propertyName == "material" || propertyName == "materials" ) && + ( typeof( Renderer ).IsAssignableFrom( currType ) || typeof( Collider ).IsAssignableFrom( currType ) || +#if !UNITY_2019_3_OR_NEWER +#pragma warning disable 0618 + typeof( GUIText ).IsAssignableFrom( currType ) || +#pragma warning restore 0618 +#endif + typeof( Collider2D ).IsAssignableFrom( currType ) ) ) + continue; + // Ignore certain Material properties that are already searched via SearchMaterial function (also, if a material doesn't have a _Color or _BaseColor + // property and its "color" property is called, it logs an error to the console, so this rule helps avoid that scenario, as well) + else if( ( propertyName == "color" || propertyName == "mainTexture" ) && typeof( Material ).IsAssignableFrom( currType ) ) + continue; + // Ignore "parameters" property of Animator since it doesn't contain any useful data and logs a warning to the console when Animator is inactive + else if( typeof( Animator ).IsAssignableFrom( currType ) && propertyName == "parameters" ) + continue; + // Ignore "spriteAnimator" property of TMP_Text component because this property adds a TMP_SpriteAnimator component to the object if it doesn't exist + else if( propertyName == "spriteAnimator" && currType.Name == "TMP_Text" ) + continue; + // Ignore "meshFilter" property of TextMeshPro and TMP_SubMesh components because this property adds a MeshFilter component to the object if it doesn't exist + else if( propertyName == "meshFilter" && ( currType.Name == "TextMeshPro" || currType.Name == "TMP_SubMesh" ) ) + continue; + // Ignore "users" property of TerrainData because it returns the Terrains in the scene that use that TerrainData. This causes issues with callStack because TerrainData + // is already in callStack when Terrains are searched via "users" property of it and hence, Terrain->TerrainData references for that TerrainData can't be found in scenes + // (this is how callStack works, it prevents searching an object if it's already in callStack to avoid infinite recursion) + else if( propertyName == "users" && typeof( TerrainData ).IsAssignableFrom( currType ) ) + continue; + else + { + VariableGetVal getter = property.CreateGetter(); + if( getter != null ) + validVariables.Add( new VariableGetterHolder( property, getter, searchSerializableVariablesOnly ? property.IsSerializable() : true ) ); + } + } + + currType = currType.BaseType; + } + } + + result = validVariables.ToArray(); + + // Cache the filtered fields + typeToVariables.Add( type, result ); + + return result; + } + + // Credit: http://answers.unity.com/answers/425602/view.html + // Returns the raw System.Object value of a SerializedProperty + private object GetRawSerializedPropertyValue( SerializedProperty property ) + { + object result = property.serializedObject.targetObject; + string[] path = property.propertyPath.Replace( ".Array.data[", "[" ).Split( '.' ); + for( int i = 0; i < path.Length; i++ ) + { + string pathElement = path[i]; + + int arrayStartIndex = pathElement.IndexOf( '[' ); + if( arrayStartIndex < 0 ) + result = GetFieldValue( result, pathElement ); + else + { + string variableName = pathElement.Substring( 0, arrayStartIndex ); + + int arrayEndIndex = pathElement.IndexOf( ']', arrayStartIndex + 1 ); + int arrayElementIndex = int.Parse( pathElement.Substring( arrayStartIndex + 1, arrayEndIndex - arrayStartIndex - 1 ) ); + result = GetFieldValue( result, variableName, arrayElementIndex ); + } + } + + return result; + } + + // Credit: http://answers.unity.com/answers/425602/view.html + private object GetFieldValue( object source, string fieldName ) + { + if( source == null ) + return null; + + FieldInfo fieldInfo = null; + Type type = source.GetType(); + while( fieldInfo == null && type != typeof( object ) ) + { + fieldInfo = type.GetField( fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly ); + type = type.BaseType; + } + + if( fieldInfo != null ) + return fieldInfo.GetValue( source ); + + PropertyInfo propertyInfo = null; + type = source.GetType(); + while( propertyInfo == null && type != typeof( object ) ) + { + propertyInfo = type.GetProperty( fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.IgnoreCase ); + type = type.BaseType; + } + + if( propertyInfo != null ) + return propertyInfo.GetValue( source, null ); + + if( fieldName.Length > 2 && fieldName.StartsWith( "m_", StringComparison.OrdinalIgnoreCase ) ) + return GetFieldValue( source, fieldName.Substring( 2 ) ); + + return null; + } + + // Credit: http://answers.unity.com/answers/425602/view.html + private object GetFieldValue( object source, string fieldName, int arrayIndex ) + { + IEnumerable enumerable = GetFieldValue( source, fieldName ) as IEnumerable; + if( enumerable == null ) + return null; + + if( enumerable is IList ) + return ( (IList) enumerable )[arrayIndex]; + + IEnumerator enumerator = enumerable.GetEnumerator(); + for( int i = 0; i <= arrayIndex; i++ ) + enumerator.MoveNext(); + + return enumerator.Current; + } + +#if ASSET_USAGE_ADDRESSABLES + private Object GetAddressablesAssetReferenceValue( AssetReference assetReference ) + { + Object result = assetReference.editorAsset; + if( !result ) + return null; + + string subObjectName = assetReference.SubObjectName; + if( !string.IsNullOrEmpty( subObjectName ) ) + { + if( result is SpriteAtlas ) + { + Sprite[] packedSprites = spriteAtlasPackedSpritesGetter( (SpriteAtlas) result ); + if( packedSprites != null ) + { + for( int i = 0; i < packedSprites.Length; i++ ) + { + if( packedSprites[i] && packedSprites[i].name == subObjectName ) + return packedSprites[i]; + } + } + } + else + { + Type subObjectType = (Type) assetReferenceSubObjectTypeGetter.GetValue( assetReference, null ) ?? typeof( Object ); + Object[] subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath( AssetDatabase.GetAssetPath( result ) ); + for( int k = 0; k < subAssets.Length; k++ ) + { + if( subAssets[k] && subAssets[k].name == subObjectName && subObjectType.IsAssignableFrom( subAssets[k].GetType() ) ) + return subAssets[k]; + } + } + } + + return result; + } +#endif + + // Iterates over all occurrences of specific key-value pairs in string + // Example1: #include "VALUE" valuePrefix=#include, valueWrapperChar=" + // Example2: "guid": "VALUE" valuePrefix="guid", valueWrapperChar=" + private void IterateOverValuesInString( string str, string[] valuePrefixes, char valueWrapperChar, Action valueAction ) + { + for( int i = 0; i < valuePrefixes.Length; i++ ) + { + string valuePrefix = valuePrefixes[i]; + int valueStartIndex, valueEndIndex = 0; + while( true ) + { + valueStartIndex = str.IndexOf( valuePrefix, valueEndIndex ); + if( valueStartIndex < 0 ) + break; + + valueStartIndex = str.IndexOf( valueWrapperChar, valueStartIndex + valuePrefix.Length ); + if( valueStartIndex < 0 ) + break; + + valueStartIndex++; + valueEndIndex = str.IndexOf( valueWrapperChar, valueStartIndex ); + if( valueEndIndex < 0 ) + break; + + if( valueEndIndex > valueStartIndex ) + valueAction( str.Substring( valueStartIndex, valueEndIndex - valueStartIndex ) ); + } + } + } + + // If obj is Component, switches to its GameObject + private object PreferablyGameObject( object obj ) + { + Component component = obj as Component; + return ( component != null && !component.Equals( null ) ) ? component.gameObject : obj; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSearchFunctions.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSearchFunctions.cs.meta new file mode 100644 index 0000000..fc1cd02 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSearchFunctions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93aaae685d4c3db44baeb91a0296855e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSettings.cs b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSettings.cs new file mode 100644 index 0000000..6dc943a --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSettings.cs @@ -0,0 +1,298 @@ +using UnityEditor; +using UnityEngine; + +namespace AssetUsageDetectorNamespace +{ + public static class AssetUsageDetectorSettings + { + private static readonly GUILayoutOption GL_WIDTH_60 = GUILayout.Width( 60f ); + + #region Colors + private static Color? m_settingsHeaderColor = null; + public static Color SettingsHeaderColor + { + get { if( m_settingsHeaderColor == null ) m_settingsHeaderColor = GetColor( "AUD_SettingsHeaderTint", Color.cyan ); return m_settingsHeaderColor.Value; } + set { if( m_settingsHeaderColor == value ) return; m_settingsHeaderColor = value; SetColor( "AUD_SettingsHeaderTint", value ); } + } + + private static Color? m_searchResultGroupHeaderColor = null; + public static Color SearchResultGroupHeaderColor + { + get { if( m_searchResultGroupHeaderColor == null ) m_searchResultGroupHeaderColor = GetColor( "AUD_ResultGroupHeaderTint", Color.cyan ); return m_searchResultGroupHeaderColor.Value; } + set { if( m_searchResultGroupHeaderColor == value ) return; m_searchResultGroupHeaderColor = value; SetColor( "AUD_ResultGroupHeaderTint", value ); } + } + + private static Color? m_rootRowsBackgroundColor = null; + public static Color RootRowsBackgroundColor + { + get { if( m_rootRowsBackgroundColor == null ) m_rootRowsBackgroundColor = GetColor( "AUD_RootRowsTint", EditorGUIUtility.isProSkin ? new Color( 0f, 1f, 1f, 0.15f ) : new Color( 0f, 1f, 1f, 0.25f ) ); return m_rootRowsBackgroundColor.Value; } + set { if( m_rootRowsBackgroundColor == value ) return; m_rootRowsBackgroundColor = value; SetColor( "AUD_RootRowsTint", value ); } + } + + private static Color? m_rootRowsBorderColor = null; + public static Color RootRowsBorderColor + { + get { if( m_rootRowsBorderColor == null ) m_rootRowsBorderColor = GetColor( "AUD_RootRowsBorderColor", EditorGUIUtility.isProSkin ? new Color( 0.15f, 0.15f, 0.15f, 1f ) : new Color( 0.375f, 0.375f, 0.375f, 1f ) ); return m_rootRowsBorderColor.Value; } + set { if( m_rootRowsBorderColor == value ) return; m_rootRowsBorderColor = value; SetColor( "AUD_RootRowsBorderColor", value ); } + } + + private static Color? m_mainReferencesBackgroundColor = null; + public static Color MainReferencesBackgroundColor + { + get { if( m_mainReferencesBackgroundColor == null ) m_mainReferencesBackgroundColor = GetColor( "AUD_MainRefRowsTint", EditorGUIUtility.isProSkin ? new Color( 0f, 0.35f, 0f, 1f ) : new Color( 0.25f, 0.75f, 0.25f, 1f ) ); return m_mainReferencesBackgroundColor.Value; } + set { if( m_mainReferencesBackgroundColor == value ) return; m_mainReferencesBackgroundColor = value; SetColor( "AUD_MainRefRowsTint", value ); } + } + + private static Color? m_selectedRowsParentTint = null; + public static Color SelectedRowParentsTint + { + get { if( m_selectedRowsParentTint == null ) m_selectedRowsParentTint = GetColor( "AUD_SelectedRowParentsTint", EditorGUIUtility.isProSkin ? new Color( 0.36f, 0.36f, 0.18f, 1f ) : new Color( 0.825f, 0.825f, 0.55f, 1f ) ); return m_selectedRowsParentTint.Value; } + set { if( m_selectedRowsParentTint == value ) return; m_selectedRowsParentTint = value; SetColor( "AUD_SelectedRowParentsTint", value ); } + } + + private static Color? m_selectedRowOccurrencesColor = null; + public static Color SelectedRowOccurrencesColor + { + get { if( m_selectedRowOccurrencesColor == null ) m_selectedRowOccurrencesColor = GetColor( "AUD_SelectedRowOccurrencesTint", EditorGUIUtility.isProSkin ? new Color( 0f, 0.3f, 0.75f, 1f ) : new Color( 0.25f, 0.75f, 1f, 1f ) ); return m_selectedRowOccurrencesColor.Value; } + set { if( m_selectedRowOccurrencesColor == value ) return; m_selectedRowOccurrencesColor = value; SetColor( "AUD_SelectedRowOccurrencesTint", value ); } + } + + private static Color? m_treeLinesColor = null; + public static Color TreeLinesColor + { + get { if( m_treeLinesColor == null ) m_treeLinesColor = GetColor( "AUD_TreeLinesColor", EditorGUIUtility.isProSkin ? new Color( 0.65f, 0.65f, 0.65f, 1f ) : new Color( 0.375f, 0.375f, 0.375f, 1f ) ); return m_treeLinesColor.Value; } + set { if( m_treeLinesColor == value ) return; m_treeLinesColor = value; SetColor( "AUD_TreeLinesColor", value ); } + } + + private static Color? m_highlightedTreeLinesColor = null; + public static Color HighlightedTreeLinesColor + { + get { if( m_highlightedTreeLinesColor == null ) m_highlightedTreeLinesColor = GetColor( "AUD_HighlightTreeLinesColor", Color.cyan ); return m_highlightedTreeLinesColor.Value; } + set { if( m_highlightedTreeLinesColor == value ) return; m_highlightedTreeLinesColor = value; SetColor( "AUD_HighlightTreeLinesColor", value ); } + } + + private static Color? m_searchMatchingTextColor = null; + public static Color SearchMatchingTextColor + { + get { if( m_searchMatchingTextColor == null ) m_searchMatchingTextColor = GetColor( "AUD_SearchTextColor", Color.red ); return m_searchMatchingTextColor.Value; } + set { if( m_searchMatchingTextColor == value ) return; m_searchMatchingTextColor = value; SetColor( "AUD_SearchTextColor", value ); ForEachAssetUsageDetectorWindow( ( window ) => window.OnSettingsChanged( highlightedSearchTextColorChanged: true ) ); } + } + + private static Color? m_tooltipDescriptionTextColor = null; + public static Color TooltipDescriptionTextColor + { + get { if( m_tooltipDescriptionTextColor == null ) m_tooltipDescriptionTextColor = GetColor( "AUD_TooltipUsageTextColor", EditorGUIUtility.isProSkin ? new Color( 0f, 0.9f, 0.9f, 1f ) : new Color( 0.9f, 0f, 0f, 1f ) ); return m_tooltipDescriptionTextColor.Value; } + set { if( m_tooltipDescriptionTextColor == value ) return; m_tooltipDescriptionTextColor = value; SetColor( "AUD_TooltipUsageTextColor", value ); ForEachAssetUsageDetectorWindow( ( window ) => window.OnSettingsChanged( tooltipDescriptionsColorChanged: true ) ); } + } + #endregion + + #region Size Adjustments + private static float? m_extraRowHeight = null; + public static float ExtraRowHeight + { + get { if( m_extraRowHeight == null ) m_extraRowHeight = EditorPrefs.GetFloat( "AUD_ExtraRowHeight", 0f ); return m_extraRowHeight.Value; } + set { if( m_extraRowHeight == value ) return; m_extraRowHeight = value; EditorPrefs.SetFloat( "AUD_ExtraRowHeight", value ); ForEachAssetUsageDetectorWindow( ( window ) => window.OnSettingsChanged() ); } + } + #endregion + + #region Other Settings + private static bool? m_showRootAssetName = null; + public static bool ShowRootAssetName + { + get { if( m_showRootAssetName == null ) m_showRootAssetName = EditorPrefs.GetBool( "AUD_ShowRootAssetName", true ); return m_showRootAssetName.Value; } + set { if( m_showRootAssetName == value ) return; m_showRootAssetName = value; EditorPrefs.SetBool( "AUD_ShowRootAssetName", value ); } + } + + private static bool? m_pingClickedObjects = null; + public static bool PingClickedObjects + { + get { if( m_pingClickedObjects == null ) m_pingClickedObjects = EditorPrefs.GetBool( "AUD_PingClickedObj", true ); return m_pingClickedObjects.Value; } + set { if( m_pingClickedObjects == value ) return; m_pingClickedObjects = value; EditorPrefs.SetBool( "AUD_PingClickedObj", value ); } + } + + private static bool? m_selectClickedObjects = null; + public static bool SelectClickedObjects + { + get { if( m_selectClickedObjects == null ) m_selectClickedObjects = EditorPrefs.GetBool( "AUD_SelectClickedObj", false ); return m_selectClickedObjects.Value; } + set { if( m_selectClickedObjects == value ) return; m_selectClickedObjects = value; EditorPrefs.SetBool( "AUD_SelectClickedObj", value ); } + } + + private static bool? m_selectDoubleClickedObjects = null; + public static bool SelectDoubleClickedObjects + { + get { if( m_selectDoubleClickedObjects == null ) m_selectDoubleClickedObjects = EditorPrefs.GetBool( "AUD_SelectDoubleClickedObj", true ); return m_selectDoubleClickedObjects.Value; } + set { if( m_selectDoubleClickedObjects == value ) return; m_selectDoubleClickedObjects = value; EditorPrefs.SetBool( "AUD_SelectDoubleClickedObj", value ); } + } + + private static bool? m_markUsedAssetsSubAssetsAsUsed = null; + public static bool MarkUsedAssetsSubAssetsAsUsed + { + get { if( m_markUsedAssetsSubAssetsAsUsed == null ) m_markUsedAssetsSubAssetsAsUsed = EditorPrefs.GetBool( "AUD_MarkUsedAssetsSubAssetsAsUsed", true ); return m_markUsedAssetsSubAssetsAsUsed.Value; } + set { if( m_markUsedAssetsSubAssetsAsUsed == value ) return; m_markUsedAssetsSubAssetsAsUsed = value; EditorPrefs.SetBool( "AUD_MarkUsedAssetsSubAssetsAsUsed", value ); } + } + + private static bool? m_showUnityTooltip = null; + public static bool ShowUnityTooltip + { + get { if( m_showUnityTooltip == null ) m_showUnityTooltip = EditorPrefs.GetBool( "AUD_ShowUnityTooltip", false ); return m_showUnityTooltip.Value; } + set { if( m_showUnityTooltip == value ) return; m_showUnityTooltip = value; EditorPrefs.SetBool( "AUD_ShowUnityTooltip", value ); } + } + + private static bool? m_showCustomTooltip = null; + public static bool ShowCustomTooltip + { + get { if( m_showCustomTooltip == null ) m_showCustomTooltip = EditorPrefs.GetBool( "AUD_ShowCustomTooltip", true ); return m_showCustomTooltip.Value; } + set { if( m_showCustomTooltip == value ) return; m_showCustomTooltip = value; EditorPrefs.SetBool( "AUD_ShowCustomTooltip", value ); ForEachAssetUsageDetectorWindow( ( window ) => window.OnSettingsChanged() ); } + } + + private static float? m_customTooltipDelay = null; + public static float CustomTooltipDelay + { + get { if( m_customTooltipDelay == null ) m_customTooltipDelay = EditorPrefs.GetFloat( "AUD_CustomTooltipDelay", 0.7f ); return m_customTooltipDelay.Value; } + set { if( m_customTooltipDelay == value ) return; m_customTooltipDelay = value; EditorPrefs.SetFloat( "AUD_CustomTooltipDelay", value ); } + } + + private static bool? m_showTreeLines = null; + public static bool ShowTreeLines + { + get { if( m_showTreeLines == null ) m_showTreeLines = EditorPrefs.GetBool( "AUD_ShowTreeLines", true ); return m_showTreeLines.Value; } + set { if( m_showTreeLines == value ) return; m_showTreeLines = value; EditorPrefs.SetBool( "AUD_ShowTreeLines", value ); } + } + + private static bool? m_applySelectedRowParentsTintToRootRows = null; + public static bool ApplySelectedRowParentsTintToRootRows + { + get { if( m_applySelectedRowParentsTintToRootRows == null ) m_applySelectedRowParentsTintToRootRows = EditorPrefs.GetBool( "AUD_SelectedRowParentsTintAtRoot", true ); return m_applySelectedRowParentsTintToRootRows.Value; } + set { if( m_applySelectedRowParentsTintToRootRows == value ) return; m_applySelectedRowParentsTintToRootRows = value; EditorPrefs.SetBool( "AUD_SelectedRowParentsTintAtRoot", value ); } + } + #endregion + +#if UNITY_2018_3_OR_NEWER + [SettingsProvider] + public static SettingsProvider CreatePreferencesGUI() + { + return new SettingsProvider( "Project/yasirkula/Asset Usage Detector", SettingsScope.Project ) + { + guiHandler = ( searchContext ) => PreferencesGUI(), + keywords = new System.Collections.Generic.HashSet() { "Asset", "Usage", "Detector" } + }; + } +#endif + +#if !UNITY_2018_3_OR_NEWER + [PreferenceItem( "Asset Usage Detector" )] +#endif + public static void PreferencesGUI() + { + float labelWidth = EditorGUIUtility.labelWidth; +#if UNITY_2018_3_OR_NEWER + EditorGUIUtility.labelWidth += 60f; +#else + EditorGUIUtility.labelWidth += 20f; +#endif + + EditorGUI.BeginChangeCheck(); + + ShowRootAssetName = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Show Root Asset's Name For Sub-Assets (Requires Refresh)", ShowRootAssetName ); + + EditorGUILayout.Space(); + + PingClickedObjects = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Ping Clicked Objects", PingClickedObjects ); + SelectClickedObjects = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Select Clicked Objects", SelectClickedObjects ); + SelectDoubleClickedObjects = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Select Double Clicked Objects", SelectDoubleClickedObjects ); + + EditorGUILayout.Space(); + + MarkUsedAssetsSubAssetsAsUsed = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Hide unused sub-assets in \"Unused Objects\" list if their parent assets are used (Requires Refresh)", MarkUsedAssetsSubAssetsAsUsed ); + + EditorGUILayout.Space(); + + ShowUnityTooltip = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Show Unity Tooltip", ShowUnityTooltip ); + ShowCustomTooltip = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Show Custom Tooltip", ShowCustomTooltip ); + EditorGUI.indentLevel++; + CustomTooltipDelay = FloatField( "Delay", CustomTooltipDelay, 0.7f ); + EditorGUI.indentLevel--; + TooltipDescriptionTextColor = ColorField( "Tooltip Descriptions Text Color", TooltipDescriptionTextColor, EditorGUIUtility.isProSkin ? new Color( 0f, 0.9f, 0.9f, 1f ) : new Color( 0.9f, 0f, 0f, 1f ) ); + + EditorGUILayout.Space(); + + ExtraRowHeight = Mathf.Max( 0f, FloatField( "Extra Row Height", ExtraRowHeight, 0f ) ); + + EditorGUILayout.Space(); + + SettingsHeaderColor = ColorField( "Settings Header Color", SettingsHeaderColor, Color.cyan ); + SearchResultGroupHeaderColor = ColorField( "Group Header Color", SearchResultGroupHeaderColor, Color.cyan ); + RootRowsBackgroundColor = ColorField( "Root Rows Background Color", RootRowsBackgroundColor, EditorGUIUtility.isProSkin ? new Color( 0f, 1f, 1f, 0.15f ) : new Color( 0f, 1f, 1f, 0.25f ) ); + RootRowsBorderColor = ColorField( "Root Rows Border Color", RootRowsBorderColor, EditorGUIUtility.isProSkin ? new Color( 0.15f, 0.15f, 0.15f, 1f ) : new Color( 0.375f, 0.375f, 0.375f, 1f ) ); + MainReferencesBackgroundColor = ColorField( "Main References Background Color", MainReferencesBackgroundColor, EditorGUIUtility.isProSkin ? new Color( 0f, 0.35f, 0f, 1f ) : new Color( 0.25f, 0.75f, 0.25f, 1f ) ); + SelectedRowParentsTint = ColorField( "Selected Row Parents Tint", SelectedRowParentsTint, EditorGUIUtility.isProSkin ? new Color( 0.36f, 0.36f, 0.18f, 1f ) : new Color( 0.825f, 0.825f, 0.55f, 1f ) ); + EditorGUI.indentLevel++; + ApplySelectedRowParentsTintToRootRows = !EditorGUILayout.Toggle( "Ignore Root Rows", !ApplySelectedRowParentsTintToRootRows ); + EditorGUI.indentLevel--; + SelectedRowOccurrencesColor = ColorField( "Selected Row All Occurrences Tint", SelectedRowOccurrencesColor, EditorGUIUtility.isProSkin ? new Color( 0f, 0.3f, 0.75f, 1f ) : new Color( 0.25f, 0.75f, 1f, 1f ) ); + SearchMatchingTextColor = ColorField( "Matching Search Text Color", SearchMatchingTextColor, Color.red ); + + ShowTreeLines = AssetUsageDetectorWindow.WordWrappingToggleLeft( "Show Tree Lines", ShowTreeLines ); + EditorGUI.indentLevel++; + TreeLinesColor = ColorField( "Normal Color", TreeLinesColor, EditorGUIUtility.isProSkin ? new Color( 0.65f, 0.65f, 0.65f, 1f ) : new Color( 0.375f, 0.375f, 0.375f, 1f ) ); + HighlightedTreeLinesColor = ColorField( "Highlighted Color", HighlightedTreeLinesColor, Color.cyan ); + EditorGUI.indentLevel--; + + EditorGUIUtility.labelWidth = labelWidth; + + if( EditorGUI.EndChangeCheck() ) + ForEachAssetUsageDetectorWindow( ( window ) => window.Repaint() ); + } + + private static Color ColorField( string label, Color value, Color defaultValue ) + { + GUILayout.BeginHorizontal(); + Color result = EditorGUILayout.ColorField( label, value ); + if( GUILayout.Button( "Reset", GL_WIDTH_60 ) ) + result = defaultValue; + GUILayout.EndHorizontal(); + + return result; + } + + private static float FloatField( string label, float value, float defaultValue ) + { + GUILayout.BeginHorizontal(); + float result = EditorGUILayout.FloatField( label, value ); + if( GUILayout.Button( "Reset", GL_WIDTH_60 ) ) + result = defaultValue; + GUILayout.EndHorizontal(); + + return result; + } + + private static Color GetColor( string pref, Color defaultColor ) + { + if( EditorGUIUtility.isProSkin ) + pref += "_Pro"; + + if( !EditorPrefs.HasKey( pref ) ) + return defaultColor; + + string[] parts = EditorPrefs.GetString( pref ).Split( ';' ); + return new Color32( byte.Parse( parts[0] ), byte.Parse( parts[1] ), byte.Parse( parts[2] ), byte.Parse( parts[3] ) ); + } + + private static void SetColor( string pref, Color32 value ) + { + if( EditorGUIUtility.isProSkin ) + pref += "_Pro"; + + EditorPrefs.SetString( pref, string.Concat( value.r.ToString(), ";", value.g.ToString(), ";", value.b.ToString(), ";", value.a.ToString() ) ); + } + + private static void ForEachAssetUsageDetectorWindow( System.Action action ) + { + foreach( AssetUsageDetectorWindow window in Resources.FindObjectsOfTypeAll() ) + { + if( window ) + action( window ); + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSettings.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSettings.cs.meta new file mode 100644 index 0000000..7b45f1f --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorSettings.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 13295073724765e45aa3b77486e515f4 +timeCreated: 1639982865 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs new file mode 100644 index 0000000..eaf276c --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs @@ -0,0 +1,812 @@ +// Asset Usage Detector - by Suleyman Yasir KULA (yasirkula@gmail.com) + +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System.Reflection; +using Object = UnityEngine.Object; +#if UNITY_2021_2_OR_NEWER +using PrefabStage = UnityEditor.SceneManagement.PrefabStage; +using PrefabStageUtility = UnityEditor.SceneManagement.PrefabStageUtility; +#elif UNITY_2018_3_OR_NEWER +using PrefabStage = UnityEditor.Experimental.SceneManagement.PrefabStage; +using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility; +#endif + +namespace AssetUsageDetectorNamespace +{ + public enum Phase { Setup, Processing, Complete }; + + public class AssetUsageDetectorWindow : EditorWindow, IHasCustomMenu + { + private enum WindowFilter { AlwaysReturnActive, ReturnActiveIfNotLocked, AlwaysReturnNew }; + + private const string PREFS_SEARCH_SCENES = "AUD_SceneSearch"; + private const string PREFS_SEARCH_SCENE_LIGHTING_SETTINGS = "AUD_LightingSettingsSearch"; + private const string PREFS_SEARCH_ASSETS = "AUD_AssetsSearch"; + private const string PREFS_SEARCH_PROJECT_SETTINGS = "AUD_ProjectSettingsSearch"; + private const string PREFS_DONT_SEARCH_SOURCE_ASSETS = "AUD_AssetsExcludeSrc"; + private const string PREFS_SEARCH_DEPTH_LIMIT = "AUD_Depth"; + private const string PREFS_SEARCH_FIELDS = "AUD_Fields"; + private const string PREFS_SEARCH_PROPERTIES = "AUD_Properties"; + private const string PREFS_SEARCH_NON_SERIALIZABLES = "AUD_NonSerializables"; + private const string PREFS_SEARCH_UNUSED_MATERIAL_PROPERTIES = "AUD_SearchUnusedMaterialProps"; + private const string PREFS_LAZY_SCENE_SEARCH = "AUD_LazySceneSearch"; + private const string PREFS_ADDRESSABLES_SUPPORT = "AUD_AddressablesSupport"; + private const string PREFS_CALCULATE_UNUSED_OBJECTS = "AUD_FindUnusedObjs"; + private const string PREFS_HIDE_DUPLICATE_ROWS = "AUD_HideDuplicates"; + private const string PREFS_HIDE_REDUNDANT_PREFAB_VARIANT_LINKS = "AUD_HideRedundantPVariantLinks"; + private const string PREFS_SHOW_PROGRESS = "AUD_Progress"; + + private static readonly GUIContent windowTitle = new GUIContent( "Asset Usage Detector" ); + private static readonly Vector2 windowMinSize = new Vector2( 325f, 220f ); + + private static readonly GUILayoutOption GL_WIDTH_12 = GUILayout.Width( 12f ); + + private GUIStyle lockButtonStyle; + + private readonly AssetUsageDetector core = new AssetUsageDetector(); + private SearchResult searchResult; // Overall search results + + // This isn't readonly so that it can be serialized + private List objectsToSearch = new List() { new ObjectToSearch( null ) }; + +#pragma warning disable 0649 + [SerializeField] // Since titleContent persists between Editor sessions, so should the IsLocked property because otherwise, "[L]" in title becomes confusing when the EditorWindow isn't actually locked + private bool m_isLocked; + private bool IsLocked + { + get { return m_isLocked; } + set + { + if( m_isLocked != value ) + { + m_isLocked = value; + titleContent = value ? new GUIContent( "[L] " + windowTitle.text, EditorGUIUtility.IconContent( "InspectorLock" ).image ) : windowTitle; + } + } + } +#pragma warning restore 0649 + + private Phase currentPhase = Phase.Setup; + + private bool searchInOpenScenes = true; // Scenes currently open in Hierarchy view + private bool searchInScenesInBuild = true; // Scenes in build + private bool searchInScenesInBuildTickedOnly = true; // Scenes in build (ticked only or not) + private bool searchInAllScenes = true; // All scenes (including scenes that are not in build) + private bool searchInSceneLightingSettings = true; // Window-Rendering-Lighting settings + private bool searchInAssetsFolder = true; // Assets in Project window + private bool dontSearchInSourceAssets = true; // objectsToSearch won't be searched for internal references + private bool searchInProjectSettings = true; // Player Settings, Graphics Settings etc. + + private List searchInAssetsSubset = new List() { null }; // If not empty, only these assets are searched for references + private List excludedAssets = new List() { null }; // These assets won't be searched for references + private List excludedScenes = new List() { null }; // These scenes won't be searched for references + + private int searchDepthLimit = 4; // Depth limit for recursively searching variables of objects + + private bool lazySceneSearch = true; +#if ASSET_USAGE_ADDRESSABLES + private bool addressablesSupport = false; +#endif + private bool searchNonSerializableVariables = true; + private bool searchUnusedMaterialProperties = true; + private bool calculateUnusedObjects = false; + private bool hideDuplicateRows = true; + private bool hideReduntantPrefabVariantLinks = true; + private bool noAssetDatabaseChanges = false; + private bool showDetailedProgressBar = true; + + private BindingFlags fieldModifiers, propertyModifiers; + + private SearchRefactoring searchRefactoring = null; // Its value can be assigned via ShowAndSearch + + private readonly ObjectToSearchListDrawer objectsToSearchDrawer = new ObjectToSearchListDrawer(); + private readonly ObjectListDrawer searchInAssetsSubsetDrawer = new ObjectListDrawer( "Search following asset(s) only:", false ); + private readonly ObjectListDrawer excludedAssetsDrawer = new ObjectListDrawer( "Don't search following asset(s):", false ); + private readonly ObjectListDrawer excludedScenesDrawer = new ObjectListDrawer( "Don't search in following scene(s):", false ); + + private bool drawObjectsToSearchSection = true; + + private Vector2 scrollPosition = Vector2.zero; + + private bool shouldRepositionSelf; + private Rect windowTargetPosition; + + void IHasCustomMenu.AddItemsToMenu( GenericMenu contextMenu ) + { + contextMenu.AddItem( new GUIContent( "Lock" ), IsLocked, () => IsLocked = !IsLocked ); + contextMenu.AddSeparator( "" ); + +#if UNITY_2018_3_OR_NEWER + contextMenu.AddItem( new GUIContent( "Settings" ), false, () => SettingsService.OpenProjectSettings( "Project/yasirkula/Asset Usage Detector" ) ); +#else + contextMenu.AddItem( new GUIContent( "Settings" ), false, () => + { + System.Type preferencesWindowType = typeof( EditorWindow ).Assembly.GetType( "UnityEditor.PreferencesWindow" ); + preferencesWindowType.GetMethod( "ShowPreferencesWindow", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).Invoke( null, null ); + + EditorWindow preferencesWindow = GetWindow( preferencesWindowType ); + if( (bool) preferencesWindowType.GetField( "m_RefreshCustomPreferences", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( preferencesWindow ) ) + { + preferencesWindowType.GetMethod( "AddCustomSections", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( preferencesWindow, null ); + preferencesWindowType.GetField( "m_RefreshCustomPreferences", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).SetValue( preferencesWindow, false ); + } + + int targetSectionIndex = -1; + System.Collections.IList sections = (System.Collections.IList) preferencesWindowType.GetField( "m_Sections", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( preferencesWindow ); + for( int i = 0; i < sections.Count; i++ ) + { + if( ( (GUIContent) sections[i].GetType().GetField( "content", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( sections[i] ) ).text == "Asset Usage Detector" ) + { + targetSectionIndex = i; + break; + } + } + + if( targetSectionIndex >= 0 ) + preferencesWindowType.GetProperty( "selectedSectionIndex", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).SetValue( preferencesWindow, targetSectionIndex, null ); + } ); +#endif + + if( currentPhase == Phase.Setup ) + { + contextMenu.AddSeparator( "" ); + contextMenu.AddItem( new GUIContent( "Refresh Sub-Assets of Searched Objects" ), false, () => + { + for( int i = objectsToSearch.Count - 1; i >= 0; i-- ) + objectsToSearch[i].RefreshSubAssets(); + } ); + } + else if( currentPhase == Phase.Complete ) + { + if( searchResult != null && searchResult.NumberOfGroups > 0 ) + { + contextMenu.AddSeparator( "" ); + contextMenu.AddItem( new GUIContent( "Collapse All" ), false, searchResult.CollapseAllSearchResultGroups ); + } + } + } + + // Shows lock button at the top-right corner + // Credit: http://leahayes.co.uk/2013/04/30/adding-the-little-padlock-button-to-your-editorwindow.html + private void ShowButton( Rect position ) + { + if( lockButtonStyle == null ) + lockButtonStyle = "IN LockButton"; + + IsLocked = GUI.Toggle( position, IsLocked, GUIContent.none, lockButtonStyle ); + } + + private static AssetUsageDetectorWindow GetWindow( WindowFilter filter ) + { + AssetUsageDetectorWindow[] windows = Resources.FindObjectsOfTypeAll(); + AssetUsageDetectorWindow window = System.Array.Find( windows, ( w ) => w && !w.IsLocked ); + if( !window ) + window = System.Array.Find( windows, ( w ) => w ); + + if( window && ( filter == WindowFilter.AlwaysReturnActive || ( !window.IsLocked && filter == WindowFilter.ReturnActiveIfNotLocked ) ) ) + { + window.Show(); + window.Focus(); + + return window; + } + + Rect? windowTargetPosition = null; + if( window ) + { + Rect position = window.position; + position.position += new Vector2( 50f, 50f ); + windowTargetPosition = position; + } + + window = CreateInstance(); + window.titleContent = windowTitle; + window.minSize = windowMinSize; + + if( windowTargetPosition.HasValue ) + { + window.shouldRepositionSelf = true; + window.windowTargetPosition = windowTargetPosition.Value; + } + + window.Show( true ); + window.Focus(); + + return window; + } + + [MenuItem( "Window/Asset Usage Detector/Active Window" )] + private static void OpenActiveWindow() + { + GetWindow( WindowFilter.AlwaysReturnActive ); + } + + [MenuItem( "Window/Asset Usage Detector/New Window" )] + private static void OpenNewWindow() + { + GetWindow( WindowFilter.AlwaysReturnNew ); + } + + // Quickly initiate search for the selected assets + [MenuItem( "GameObject/Search for References/This Object Only", priority = 49 )] + [MenuItem( "Assets/Search for References", priority = 1000 )] + private static void SearchSelectedAssetReferences( MenuCommand command ) + { + // This happens when this button is clicked via hierarchy's right click context menu + // and is called once for each object in the selection. We don't want that, we want + // the function to be called only once + if( command.context ) + { + EditorApplication.update -= CallSearchSelectedAssetReferencesOnce; + EditorApplication.update += CallSearchSelectedAssetReferencesOnce; + } + else + ShowAndSearch( Selection.objects ); + } + + [MenuItem( "GameObject/Search for References/Include Children", priority = 49 )] + private static void SearchSelectedAssetReferencesWithChildren( MenuCommand command ) + { + if( command.context ) + { + EditorApplication.update -= CallSearchSelectedAssetReferencesWithChildrenOnce; + EditorApplication.update += CallSearchSelectedAssetReferencesWithChildrenOnce; + } + else + ShowAndSearch( Selection.objects, true ); + } + + // Show the menu item only if there is a selection in the Editor + [MenuItem( "GameObject/Search for References/This Object Only", validate = true )] + [MenuItem( "GameObject/Search for References/Include Children", validate = true )] + [MenuItem( "Assets/Search for References", validate = true )] + private static bool SearchSelectedAssetReferencesValidate( MenuCommand command ) + { + return Selection.objects.Length > 0; + } + + // Quickly show the AssetUsageDetector window and initiate a search + public static void ShowAndSearch( IEnumerable searchObjects, bool? shouldSearchChildren = null ) + { + GetWindow( WindowFilter.ReturnActiveIfNotLocked ).ShowAndSearchInternal( searchObjects, null, shouldSearchChildren ); + } + + // Quickly show the AssetUsageDetector window and initiate a search + public static void ShowAndSearch( AssetUsageDetector.Parameters searchParameters, bool? shouldSearchChildren = null ) + { + if( searchParameters == null ) + { + Debug.LogError( "searchParameters can't be null!" ); + return; + } + + GetWindow( WindowFilter.ReturnActiveIfNotLocked ).ShowAndSearchInternal( searchParameters.objectsToSearch, searchParameters, shouldSearchChildren ); + } + + private static void CallSearchSelectedAssetReferencesOnce() + { + EditorApplication.update -= CallSearchSelectedAssetReferencesOnce; + SearchSelectedAssetReferences( new MenuCommand( null ) ); + } + + private static void CallSearchSelectedAssetReferencesWithChildrenOnce() + { + EditorApplication.update -= CallSearchSelectedAssetReferencesWithChildrenOnce; + SearchSelectedAssetReferencesWithChildren( new MenuCommand( null ) ); + } + + private void ShowAndSearchInternal( IEnumerable searchObjects, AssetUsageDetector.Parameters searchParameters, bool? shouldSearchChildren ) + { + if( !ReturnToSetupPhase() ) + { + Debug.LogError( "Need to reset the previous search first!" ); + return; + } + + objectsToSearch.Clear(); + if( searchObjects != null ) + { + foreach( Object obj in searchObjects ) + objectsToSearch.Add( new ObjectToSearch( obj, shouldSearchChildren ) ); + } + + if( searchParameters != null ) + { + ParseSceneSearchMode( searchParameters.searchInScenes ); + searchInSceneLightingSettings = searchParameters.searchInSceneLightingSettings; + searchInAssetsFolder = searchParameters.searchInAssetsFolder; + dontSearchInSourceAssets = searchParameters.dontSearchInSourceAssets; + searchInProjectSettings = searchParameters.searchInProjectSettings; + searchDepthLimit = searchParameters.searchDepthLimit; + fieldModifiers = searchParameters.fieldModifiers; + propertyModifiers = searchParameters.propertyModifiers; + searchNonSerializableVariables = searchParameters.searchNonSerializableVariables; + searchUnusedMaterialProperties = searchParameters.searchUnusedMaterialProperties; + searchRefactoring = searchParameters.searchRefactoring; + lazySceneSearch = searchParameters.lazySceneSearch; +#if ASSET_USAGE_ADDRESSABLES + addressablesSupport = searchParameters.addressablesSupport; +#endif + calculateUnusedObjects = searchParameters.calculateUnusedObjects; + hideDuplicateRows = searchParameters.hideDuplicateRows; + hideReduntantPrefabVariantLinks = searchParameters.hideReduntantPrefabVariantLinks; + noAssetDatabaseChanges = searchParameters.noAssetDatabaseChanges; + showDetailedProgressBar = searchParameters.showDetailedProgressBar; + + searchInAssetsSubset.Clear(); + if( searchParameters.searchInAssetsSubset != null ) + { + foreach( Object obj in searchParameters.searchInAssetsSubset ) + searchInAssetsSubset.Add( obj ); + } + + excludedAssets.Clear(); + if( searchParameters.excludedAssetsFromSearch != null ) + { + foreach( Object obj in searchParameters.excludedAssetsFromSearch ) + excludedAssets.Add( obj ); + } + + excludedScenes.Clear(); + if( searchParameters.excludedScenesFromSearch != null ) + { + foreach( Object obj in searchParameters.excludedScenesFromSearch ) + excludedScenes.Add( obj ); + } + } + + InitiateSearch(); + Repaint(); + } + + private void Awake() + { + LoadPrefs(); + } + + private void OnEnable() + { + if( currentPhase == Phase.Complete && AssetUsageDetectorSettings.ShowCustomTooltip ) + wantsMouseMove = wantsMouseEnterLeaveWindow = true; // These values aren't preserved during domain reload on Unity 2020.3.0f1 + +#if UNITY_2018_3_OR_NEWER + PrefabStage.prefabStageClosing -= ReplacePrefabStageObjectsWithAssets; + PrefabStage.prefabStageClosing += ReplacePrefabStageObjectsWithAssets; +#endif + } + + private void OnDisable() + { +#if UNITY_2018_3_OR_NEWER + PrefabStage.prefabStageClosing -= ReplacePrefabStageObjectsWithAssets; +#endif + SearchResultTooltip.Hide(); + } + + private void OnDestroy() + { + if( core != null ) + core.SaveCache(); + + SavePrefs(); + + if( searchResult != null && currentPhase == Phase.Complete ) + searchResult.RestoreInitialSceneSetup(); + } + + private void SavePrefs() + { + EditorPrefs.SetInt( PREFS_SEARCH_SCENES, (int) GetSceneSearchMode( false ) ); + EditorPrefs.SetBool( PREFS_SEARCH_SCENE_LIGHTING_SETTINGS, searchInSceneLightingSettings ); + EditorPrefs.SetBool( PREFS_SEARCH_ASSETS, searchInAssetsFolder ); + EditorPrefs.SetBool( PREFS_DONT_SEARCH_SOURCE_ASSETS, dontSearchInSourceAssets ); + EditorPrefs.SetBool( PREFS_SEARCH_PROJECT_SETTINGS, searchInProjectSettings ); + EditorPrefs.SetInt( PREFS_SEARCH_DEPTH_LIMIT, searchDepthLimit ); + EditorPrefs.SetInt( PREFS_SEARCH_FIELDS, (int) fieldModifiers ); + EditorPrefs.SetInt( PREFS_SEARCH_PROPERTIES, (int) propertyModifiers ); + EditorPrefs.SetBool( PREFS_SEARCH_NON_SERIALIZABLES, searchNonSerializableVariables ); + EditorPrefs.SetBool( PREFS_SEARCH_UNUSED_MATERIAL_PROPERTIES, searchUnusedMaterialProperties ); + EditorPrefs.SetBool( PREFS_LAZY_SCENE_SEARCH, lazySceneSearch ); +#if ASSET_USAGE_ADDRESSABLES + EditorPrefs.SetBool( PREFS_ADDRESSABLES_SUPPORT, addressablesSupport ); +#endif + EditorPrefs.SetBool( PREFS_CALCULATE_UNUSED_OBJECTS, calculateUnusedObjects ); + EditorPrefs.SetBool( PREFS_HIDE_DUPLICATE_ROWS, hideDuplicateRows ); + EditorPrefs.SetBool( PREFS_HIDE_REDUNDANT_PREFAB_VARIANT_LINKS, hideReduntantPrefabVariantLinks ); + EditorPrefs.SetBool( PREFS_SHOW_PROGRESS, showDetailedProgressBar ); + } + + private void LoadPrefs() + { + ParseSceneSearchMode( (SceneSearchMode) EditorPrefs.GetInt( PREFS_SEARCH_SCENES, (int) ( SceneSearchMode.OpenScenes | SceneSearchMode.ScenesInBuildSettingsTickedOnly | SceneSearchMode.AllScenes ) ) ); + searchInSceneLightingSettings = EditorPrefs.GetBool( PREFS_SEARCH_SCENE_LIGHTING_SETTINGS, true ); + searchInAssetsFolder = EditorPrefs.GetBool( PREFS_SEARCH_ASSETS, true ); + dontSearchInSourceAssets = EditorPrefs.GetBool( PREFS_DONT_SEARCH_SOURCE_ASSETS, true ); + searchInProjectSettings = EditorPrefs.GetBool( PREFS_SEARCH_PROJECT_SETTINGS, true ); + searchDepthLimit = EditorPrefs.GetInt( PREFS_SEARCH_DEPTH_LIMIT, 4 ); + fieldModifiers = (BindingFlags) EditorPrefs.GetInt( PREFS_SEARCH_FIELDS, (int) ( BindingFlags.Public | BindingFlags.NonPublic ) ); + propertyModifiers = (BindingFlags) EditorPrefs.GetInt( PREFS_SEARCH_PROPERTIES, (int) ( BindingFlags.Public | BindingFlags.NonPublic ) ); + searchNonSerializableVariables = EditorPrefs.GetBool( PREFS_SEARCH_NON_SERIALIZABLES, true ); + searchUnusedMaterialProperties = EditorPrefs.GetBool( PREFS_SEARCH_UNUSED_MATERIAL_PROPERTIES, true ); + lazySceneSearch = EditorPrefs.GetBool( PREFS_LAZY_SCENE_SEARCH, true ); +#if ASSET_USAGE_ADDRESSABLES + addressablesSupport = EditorPrefs.GetBool( PREFS_ADDRESSABLES_SUPPORT, false ); +#endif + calculateUnusedObjects = EditorPrefs.GetBool( PREFS_CALCULATE_UNUSED_OBJECTS, false ); + hideDuplicateRows = EditorPrefs.GetBool( PREFS_HIDE_DUPLICATE_ROWS, true ); + hideReduntantPrefabVariantLinks = EditorPrefs.GetBool( PREFS_HIDE_REDUNDANT_PREFAB_VARIANT_LINKS, true ); + showDetailedProgressBar = EditorPrefs.GetBool( PREFS_SHOW_PROGRESS, true ); + } + + private SceneSearchMode GetSceneSearchMode( bool hideOptionsInPlayMode ) + { + SceneSearchMode sceneSearchMode = SceneSearchMode.None; + if( searchInOpenScenes ) + sceneSearchMode |= SceneSearchMode.OpenScenes; + if( !hideOptionsInPlayMode || !EditorApplication.isPlaying ) + { + if( searchInScenesInBuild ) + sceneSearchMode |= searchInScenesInBuildTickedOnly ? SceneSearchMode.ScenesInBuildSettingsTickedOnly : SceneSearchMode.ScenesInBuildSettingsAll; + if( searchInAllScenes ) + sceneSearchMode |= SceneSearchMode.AllScenes; + } + + return sceneSearchMode; + } + + private void ParseSceneSearchMode( SceneSearchMode sceneSearchMode ) + { + searchInOpenScenes = ( sceneSearchMode & SceneSearchMode.OpenScenes ) == SceneSearchMode.OpenScenes; + searchInScenesInBuild = ( sceneSearchMode & SceneSearchMode.ScenesInBuildSettingsAll ) == SceneSearchMode.ScenesInBuildSettingsAll || ( sceneSearchMode & SceneSearchMode.ScenesInBuildSettingsTickedOnly ) == SceneSearchMode.ScenesInBuildSettingsTickedOnly; + searchInScenesInBuildTickedOnly = ( sceneSearchMode & SceneSearchMode.ScenesInBuildSettingsAll ) != SceneSearchMode.ScenesInBuildSettingsAll; + searchInAllScenes = ( sceneSearchMode & SceneSearchMode.AllScenes ) == SceneSearchMode.AllScenes; + } + + private void Update() + { + if( shouldRepositionSelf ) + { + shouldRepositionSelf = false; + position = windowTargetPosition; + } + } + + private void OnGUI() + { + // Make the window scrollable + scrollPosition = EditorGUILayout.BeginScrollView( scrollPosition, Utilities.GL_EXPAND_WIDTH, Utilities.GL_EXPAND_HEIGHT ); + + GUILayout.BeginVertical(); + + if( currentPhase == Phase.Processing ) + { + // If we are stuck at this phase, then we have encountered an exception + GUILayout.Label( ". . . Search in progress or something went wrong (check console) . . ." ); + + if( GUILayout.Button( "RETURN", Utilities.GL_HEIGHT_30 ) ) + { + ReturnToSetupPhase(); + GUIUtility.ExitGUI(); + } + } + else if( currentPhase == Phase.Setup ) + { + DrawObjectsToSearchSection(); + + GUILayout.Space( 10f ); + + Color c = GUI.backgroundColor; + GUI.backgroundColor = AssetUsageDetectorSettings.SettingsHeaderColor; + GUILayout.Box( "SEARCH IN", Utilities.BoxGUIStyle, Utilities.GL_EXPAND_WIDTH ); + GUI.backgroundColor = c; + + searchInAssetsFolder = WordWrappingToggleLeft( "Project window (Assets folder)", searchInAssetsFolder ); + + if( searchInAssetsFolder ) + { + GUILayout.BeginHorizontal(); + GUILayout.Space( 35f ); + GUILayout.BeginVertical(); + + searchInAssetsSubsetDrawer.Draw( searchInAssetsSubset ); + excludedAssetsDrawer.Draw( excludedAssets ); + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + GUILayout.Space( 5f ); + + dontSearchInSourceAssets = WordWrappingToggleLeft( "Don't search \"SEARCHED OBJECTS\" themselves for references", dontSearchInSourceAssets ); + searchUnusedMaterialProperties = WordWrappingToggleLeft( "Search unused material properties (e.g. normal map of a material that no longer uses normal mapping)", searchUnusedMaterialProperties ); + + Utilities.DrawSeparatorLine(); + + if( searchInAllScenes && !EditorApplication.isPlaying ) + GUI.enabled = false; + + searchInOpenScenes = WordWrappingToggleLeft( "Currently open (loaded) scene(s)", searchInOpenScenes ); + + if( !EditorApplication.isPlaying ) + { + searchInScenesInBuild = WordWrappingToggleLeft( "Scenes in Build Settings", searchInScenesInBuild ); + + if( searchInScenesInBuild ) + { + GUILayout.BeginHorizontal(); + GUILayout.Space( 35f ); + + searchInScenesInBuildTickedOnly = EditorGUILayout.ToggleLeft( "Ticked only", searchInScenesInBuildTickedOnly, Utilities.GL_WIDTH_100 ); + searchInScenesInBuildTickedOnly = !EditorGUILayout.ToggleLeft( "All", !searchInScenesInBuildTickedOnly, Utilities.GL_WIDTH_100 ); + + GUILayout.EndHorizontal(); + } + + GUI.enabled = true; + + searchInAllScenes = WordWrappingToggleLeft( "All scenes in the project", searchInAllScenes ); + } + + GUILayout.BeginHorizontal(); + GUILayout.Space( 35f ); + GUILayout.BeginVertical(); + + excludedScenesDrawer.Draw( excludedScenes ); + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + + EditorGUI.BeginDisabledGroup( !searchInOpenScenes && !searchInScenesInBuild && !searchInAllScenes ); + searchInSceneLightingSettings = WordWrappingToggleLeft( "Scene Lighting Settings (WARNING: This may change the active scene during search)", searchInSceneLightingSettings ); + EditorGUI.EndDisabledGroup(); + + Utilities.DrawSeparatorLine(); + + searchInProjectSettings = WordWrappingToggleLeft( "Project Settings (Player Settings, Graphics Settings etc.)", searchInProjectSettings ); + + GUILayout.Space( 10f ); + + GUI.backgroundColor = AssetUsageDetectorSettings.SettingsHeaderColor; + GUILayout.Box( "SETTINGS", Utilities.BoxGUIStyle, Utilities.GL_EXPAND_WIDTH ); + GUI.backgroundColor = c; + +#if ASSET_USAGE_ADDRESSABLES + EditorGUI.BeginDisabledGroup( addressablesSupport ); +#endif + lazySceneSearch = WordWrappingToggleLeft( "Lazy scene search: scenes are searched in detail only when they are manually refreshed (faster search)", lazySceneSearch ); +#if ASSET_USAGE_ADDRESSABLES + EditorGUI.EndDisabledGroup(); + addressablesSupport = WordWrappingToggleLeft( "Addressables support (Experimental) (WARNING: 'Lazy scene search' will be disabled) (slower search)", addressablesSupport ); +#endif + calculateUnusedObjects = WordWrappingToggleLeft( "Calculate unused objects", calculateUnusedObjects ); + hideDuplicateRows = WordWrappingToggleLeft( "Hide duplicate rows in search results", hideDuplicateRows ); +#if UNITY_2018_3_OR_NEWER + hideReduntantPrefabVariantLinks = WordWrappingToggleLeft( "Hide redundant prefab variant links (when the same value is assigned to the same Component of a prefab and its variant(s))", hideReduntantPrefabVariantLinks ); +#endif + noAssetDatabaseChanges = WordWrappingToggleLeft( "I haven't modified any assets/scenes since the last search (faster search)", noAssetDatabaseChanges ); + showDetailedProgressBar = WordWrappingToggleLeft( "Update search progress bar more often (cancelable search) (slower search)", showDetailedProgressBar ); + + GUILayout.Space( 10f ); + + // Don't let the user press the GO button without any valid search location + if( !searchInAllScenes && !searchInOpenScenes && !searchInScenesInBuild && !searchInAssetsFolder && !searchInProjectSettings ) + GUI.enabled = false; + + if( GUILayout.Button( "GO!", Utilities.GL_HEIGHT_30 ) ) + { + InitiateSearch(); + GUIUtility.ExitGUI(); + } + + GUILayout.Space( 5f ); + } + else if( currentPhase == Phase.Complete ) + { + // Draw the results of the search + GUI.enabled = false; + + DrawObjectsToSearchSection(); + + if( drawObjectsToSearchSection ) + GUILayout.Space( 10f ); + + GUI.enabled = true; + + if( GUILayout.Button( "Reset Search", Utilities.GL_HEIGHT_30 ) ) + { + ReturnToSetupPhase(); + GUIUtility.ExitGUI(); + } + + if( searchResult == null ) + { + EditorGUILayout.HelpBox( "ERROR: searchResult is null", MessageType.Error ); + return; + } + else if( !searchResult.SearchCompletedSuccessfully ) + EditorGUILayout.HelpBox( "ERROR: search was interrupted, check the logs for more info", MessageType.Error ); + + if( searchResult.NumberOfGroups == 0 ) + { + GUILayout.Space( 10f ); + GUILayout.Box( "No references found...", Utilities.BoxGUIStyle, Utilities.GL_EXPAND_WIDTH ); + } + else + { + noAssetDatabaseChanges = WordWrappingToggleLeft( "I haven't modified any assets/scenes since the last search (faster Refresh)", noAssetDatabaseChanges ); + + EditorGUILayout.Space(); + + scrollPosition.y = searchResult.DrawOnGUI( this, scrollPosition.y, noAssetDatabaseChanges ); + } + } + + if( Event.current.type == EventType.MouseLeaveWindow ) + { + SearchResultTooltip.Hide(); + + if( searchResult != null ) + searchResult.CancelDelayedTreeViewTooltip(); + } + + GUILayout.EndVertical(); + + EditorGUILayout.EndScrollView(); + } + + private void DrawObjectsToSearchSection() + { + Color c = GUI.backgroundColor; + GUI.backgroundColor = AssetUsageDetectorSettings.SettingsHeaderColor; + GUILayout.Box( "SEARCHED OBJECTS", Utilities.BoxGUIStyle, Utilities.GL_EXPAND_WIDTH ); + GUI.backgroundColor = c; + + Rect searchedObjectsHeaderRect = GUILayoutUtility.GetLastRect(); + searchedObjectsHeaderRect.x += 5f; + searchedObjectsHeaderRect.yMin += ( searchedObjectsHeaderRect.height - EditorGUIUtility.singleLineHeight ) * 0.5f; + searchedObjectsHeaderRect.height = EditorGUIUtility.singleLineHeight; + + drawObjectsToSearchSection = EditorGUI.Foldout( searchedObjectsHeaderRect, drawObjectsToSearchSection, GUIContent.none, true ); + + if( drawObjectsToSearchSection ) + objectsToSearchDrawer.Draw( objectsToSearch ); + } + + public static bool WordWrappingToggleLeft( string label, bool value ) + { + GUILayout.BeginHorizontal(); + bool result = EditorGUILayout.ToggleLeft( GUIContent.none, value, GL_WIDTH_12 ); + if( GUILayout.Button( label, EditorStyles.wordWrappedLabel ) ) + { + GUI.FocusControl( null ); + result = !value; + } + GUILayout.EndHorizontal(); + + return result; + } + + private void InitiateSearch() + { + currentPhase = Phase.Processing; + + SavePrefs(); + +#if UNITY_2018_3_OR_NEWER + ReplacePrefabStageObjectsWithAssets( PrefabStageUtility.GetCurrentPrefabStage() ); +#endif + + // Start searching + searchResult = core.Run( new AssetUsageDetector.Parameters() + { + objectsToSearch = !objectsToSearch.IsEmpty() ? new ObjectToSearchEnumerator( objectsToSearch ).ToArray() : null, + searchInScenes = GetSceneSearchMode( true ), + searchInSceneLightingSettings = searchInSceneLightingSettings, + searchInAssetsFolder = searchInAssetsFolder, + searchInAssetsSubset = !searchInAssetsSubset.IsEmpty() ? searchInAssetsSubset.ToArray() : null, + excludedAssetsFromSearch = !excludedAssets.IsEmpty() ? excludedAssets.ToArray() : null, + dontSearchInSourceAssets = dontSearchInSourceAssets, + excludedScenesFromSearch = !excludedScenes.IsEmpty() ? excludedScenes.ToArray() : null, + searchInProjectSettings = searchInProjectSettings, + //fieldModifiers = fieldModifiers, + //propertyModifiers = propertyModifiers, + //searchDepthLimit = searchDepthLimit, + //searchNonSerializableVariables = searchNonSerializableVariables, + searchUnusedMaterialProperties = searchUnusedMaterialProperties, + searchRefactoring = searchRefactoring, +#if ASSET_USAGE_ADDRESSABLES + lazySceneSearch = lazySceneSearch && !addressablesSupport, + addressablesSupport = addressablesSupport, +#else + lazySceneSearch = lazySceneSearch, +#endif + calculateUnusedObjects = calculateUnusedObjects, + hideDuplicateRows = hideDuplicateRows, + hideReduntantPrefabVariantLinks = hideReduntantPrefabVariantLinks, + noAssetDatabaseChanges = noAssetDatabaseChanges, + showDetailedProgressBar = showDetailedProgressBar + } ); + + currentPhase = Phase.Complete; + + // We really don't want SearchRefactoring to affect next searches unless the search is initiated via ShowAndSearch again + searchRefactoring = null; + + if( AssetUsageDetectorSettings.ShowCustomTooltip ) + wantsMouseMove = wantsMouseEnterLeaveWindow = true; + } + +#if UNITY_2018_3_OR_NEWER + // Try replacing searched objects who are part of currently open prefab stage with their corresponding prefab assets + public void ReplacePrefabStageObjectsWithAssets( PrefabStage prefabStage ) + { + if( prefabStage == null || !prefabStage.stageHandle.IsValid() ) + return; + +#if UNITY_2020_1_OR_NEWER + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath( prefabStage.assetPath ); +#else + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath( prefabStage.prefabAssetPath ); +#endif + if( prefabAsset == null || prefabAsset.Equals( null ) ) + return; + + for( int i = 0; i < objectsToSearch.Count; i++ ) + { + Object obj = objectsToSearch[i].obj; + if( obj != null && !obj.Equals( null ) && obj is GameObject && prefabStage.IsPartOfPrefabContents( (GameObject) obj ) ) + { + GameObject prefabStageObjectSource = ( (GameObject) obj ).FollowSymmetricHierarchy( prefabStage.prefabContentsRoot, prefabAsset ); + if( prefabStageObjectSource != null ) + objectsToSearch[i].obj = prefabStageObjectSource; + + List subAssets = objectsToSearch[i].subAssets; + for( int j = 0; j < subAssets.Count; j++ ) + { + obj = subAssets[j].subAsset; + if( obj != null && !obj.Equals( null ) && obj is GameObject && prefabStage.IsPartOfPrefabContents( (GameObject) obj ) ) + { + prefabStageObjectSource = ( (GameObject) obj ).FollowSymmetricHierarchy( prefabStage.prefabContentsRoot, prefabAsset ); + if( prefabStageObjectSource != null ) + subAssets[j].subAsset = prefabStageObjectSource; + } + } + } + } + } +#endif + + private bool ReturnToSetupPhase() + { + if( searchResult != null && !EditorApplication.isPlaying && !searchResult.RestoreInitialSceneSetup() ) + return false; + + searchResult = null; + currentPhase = Phase.Setup; + wantsMouseMove = wantsMouseEnterLeaveWindow = false; + + SearchResultTooltip.Hide(); + + return true; + } + + internal void OnSettingsChanged( bool highlightedSearchTextColorChanged = false, bool tooltipDescriptionsColorChanged = false ) + { + if( searchResult == null ) + return; + + wantsMouseMove = wantsMouseEnterLeaveWindow = AssetUsageDetectorSettings.ShowCustomTooltip; + + for( int i = searchResult.NumberOfGroups - 1; i >= 0; i-- ) + { + if( searchResult[i].treeView != null ) + { + searchResult[i].treeView.rowHeight = EditorGUIUtility.singleLineHeight + AssetUsageDetectorSettings.ExtraRowHeight; + searchResult[i].treeView.OnSettingsChanged( highlightedSearchTextColorChanged, tooltipDescriptionsColorChanged ); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs.meta new file mode 100644 index 0000000..95c1c9f --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 271a22c69c3d96c4dbdd04cca415a840 +timeCreated: 1520032279 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/Enumerators.cs b/Assets/Plugins/AssetUsageDetector/Editor/Enumerators.cs new file mode 100644 index 0000000..2280128 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/Enumerators.cs @@ -0,0 +1,130 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace AssetUsageDetectorNamespace +{ + public class EmptyEnumerator : IEnumerable, IEnumerator + { + public T Current { get { return default( T ); } } + object IEnumerator.Current { get { return Current; } } + + public void Dispose() { } + public void Reset() { } + + public bool MoveNext() + { + return false; + } + + public IEnumerator GetEnumerator() + { + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this; + } + } + + public class ObjectToSearchEnumerator : IEnumerable + { + public class Enumerator : IEnumerator + { + public Object Current + { + get + { + if( subAssetIndex < 0 ) + return source[index].obj; + + return source[index].subAssets[subAssetIndex].subAsset; + } + } + + object IEnumerator.Current { get { return Current; } } + + private List source; + private int index; + private int subAssetIndex; + + public Enumerator( List source ) + { + this.source = source; + Reset(); + } + + public void Dispose() + { + source = null; + } + + public bool MoveNext() + { + if( subAssetIndex < -1 ) + { + subAssetIndex = -1; + + if( ++index >= source.Count ) + return false; + + // Skip folder assets in the enumeration, AssetUsageDetector expands encountered folders automatically + // and we don't want that to happen as source[index].subAssets already contains the folder's contents + if( !source[index].obj.IsFolder() ) + return true; + } + + List subAssets = source[index].subAssets; + if( subAssets != null ) + { + while( ++subAssetIndex < subAssets.Count && !subAssets[subAssetIndex].shouldSearch ) + continue; + + if( subAssetIndex < subAssets.Count ) + return true; + } + + subAssetIndex = -2; + return MoveNext(); + } + + public void Reset() + { + index = -1; + subAssetIndex = -2; + } + } + + private readonly List source; + + public ObjectToSearchEnumerator( List source ) + { + this.source = source; + } + + public IEnumerator GetEnumerator() + { + return new Enumerator( source ); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Object[] ToArray() + { + int count = 0; + foreach( Object obj in this ) + count++; + + Object[] result = new Object[count]; + int index = 0; + foreach( Object obj in this ) + result[index++] = obj; + + return result; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/Enumerators.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/Enumerators.cs.meta new file mode 100644 index 0000000..498a808 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/Enumerators.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 894047c47ce45cf40939dae24afcc72b +timeCreated: 1562079461 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/ListDrawer.cs b/Assets/Plugins/AssetUsageDetector/Editor/ListDrawer.cs new file mode 100644 index 0000000..d7d50dd --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/ListDrawer.cs @@ -0,0 +1,252 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace AssetUsageDetectorNamespace +{ + public abstract class ListDrawer + { + private readonly string label; + private readonly bool acceptSceneObjects; + + protected ListDrawer( string label, bool acceptSceneObjects ) + { + this.label = label; + this.acceptSceneObjects = acceptSceneObjects; + } + + // Exposes a list on GUI + public bool Draw( List list ) + { + bool hasChanged = false; + bool guiEnabled = GUI.enabled; + + Event ev = Event.current; + + GUILayout.BeginHorizontal(); + + GUILayout.Label( label ); + + if( guiEnabled ) + { + // Handle drag & drop references to array + // Credit: https://answers.unity.com/answers/657877/view.html + if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) + { + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + if( ev.type == EventType.DragPerform ) + { + DragAndDrop.AcceptDrag(); + + Object[] draggedObjects = DragAndDrop.objectReferences; + if( draggedObjects.Length > 0 ) + { + for( int i = 0; i < draggedObjects.Length; i++ ) + { + if( draggedObjects[i] != null && !draggedObjects[i].Equals( null ) ) + { + bool replacedNullElement = false; + for( int j = 0; j < list.Count; j++ ) + { + if( IsElementNull( list[j] ) ) + { + list[j] = CreateElement( draggedObjects[i] ); + + replacedNullElement = true; + break; + } + } + + if( !replacedNullElement ) + list.Add( CreateElement( draggedObjects[i] ) ); + + hasChanged = true; + } + } + } + } + + ev.Use(); + } + else if( ev.type == EventType.ContextClick && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) + { + GenericMenu contextMenu = new GenericMenu(); + contextMenu.AddItem( new GUIContent( "Clear" ), false, () => + { + list.Clear(); + list.Add( CreateElement( null ) ); + } ); + contextMenu.ShowAsContext(); + + ev.Use(); + } + + if( GUILayout.Button( "+", Utilities.GL_WIDTH_25 ) ) + list.Insert( 0, CreateElement( null ) ); + } + + GUILayout.EndHorizontal(); + + for( int i = 0; i < list.Count; i++ ) + { + T element = list[i]; + + GUI.changed = false; + GUILayout.BeginHorizontal(); + + Object prevObject = GetObjectFromElement( element ); + Object newObject = EditorGUILayout.ObjectField( "", prevObject, typeof( Object ), acceptSceneObjects ); + + if( GUI.changed ) + { + hasChanged = true; + SetObjectOfElement( list, i, newObject ); + } + + if( guiEnabled ) + { + if( GUILayout.Button( "+", Utilities.GL_WIDTH_25 ) ) + list.Insert( i + 1, CreateElement( null ) ); + + if( GUILayout.Button( "-", Utilities.GL_WIDTH_25 ) ) + { + if( element != null && !element.Equals( null ) ) + hasChanged = true; + + // Lists with no elements look ugly, always keep a dummy null variable + if( list.Count > 1 ) + list.RemoveAt( i-- ); + else + list[0] = CreateElement( null ); + } + } + + GUILayout.EndHorizontal(); + + PostElementDrawer( element ); + } + + return hasChanged; + } + + protected abstract T CreateElement( Object source ); + protected abstract Object GetObjectFromElement( T element ); + protected abstract void SetObjectOfElement( List list, int index, Object value ); + protected abstract bool IsElementNull( T element ); + protected abstract void PostElementDrawer( T element ); + } + + public class ObjectListDrawer : ListDrawer + { + public ObjectListDrawer( string label, bool acceptSceneObjects ) : base( label, acceptSceneObjects ) + { + } + + protected override Object CreateElement( Object source ) + { + return source; + } + + protected override Object GetObjectFromElement( Object element ) + { + return element; + } + + protected override void SetObjectOfElement( List list, int index, Object value ) + { + list[index] = value; + } + + protected override bool IsElementNull( Object element ) + { + return element == null || element.Equals( null ); + } + + protected override void PostElementDrawer( Object element ) + { + } + } + + public class ObjectToSearchListDrawer : ListDrawer + { + public ObjectToSearchListDrawer() : base( "Find references of:", true ) + { + } + + protected override ObjectToSearch CreateElement( Object source ) + { + return new ObjectToSearch( source ); + } + + protected override Object GetObjectFromElement( ObjectToSearch element ) + { + return element.obj; + } + + protected override void SetObjectOfElement( List list, int index, Object value ) + { + list[index].obj = value; + list[index].RefreshSubAssets(); + } + + protected override bool IsElementNull( ObjectToSearch element ) + { + return element == null || element.obj == null || element.obj.Equals( null ); + } + + protected override void PostElementDrawer( ObjectToSearch element ) + { + List subAssetsToSearch = element.subAssets; + if( subAssetsToSearch.Count > 0 ) + { + GUILayout.BeginHorizontal(); + + // 0-> all toggles off, 1-> mixed, 2-> all toggles on + bool toggleAllSubAssets = subAssetsToSearch[0].shouldSearch; + bool mixedToggle = false; + for( int j = 1; j < subAssetsToSearch.Count; j++ ) + { + if( subAssetsToSearch[j].shouldSearch != toggleAllSubAssets ) + { + mixedToggle = true; + break; + } + } + + if( mixedToggle ) + EditorGUI.showMixedValue = true; + + GUI.changed = false; + toggleAllSubAssets = EditorGUILayout.Toggle( toggleAllSubAssets, Utilities.GL_WIDTH_25 ); + if( GUI.changed ) + { + for( int j = 0; j < subAssetsToSearch.Count; j++ ) + subAssetsToSearch[j].shouldSearch = toggleAllSubAssets; + } + + EditorGUI.showMixedValue = false; + + element.showSubAssetsFoldout = EditorGUILayout.Foldout( element.showSubAssetsFoldout, "Include sub-assets in search:", true ); + + GUILayout.EndHorizontal(); + + if( element.showSubAssetsFoldout ) + { + for( int j = 0; j < subAssetsToSearch.Count; j++ ) + { + GUILayout.BeginHorizontal(); + + subAssetsToSearch[j].shouldSearch = EditorGUILayout.Toggle( subAssetsToSearch[j].shouldSearch, Utilities.GL_WIDTH_25 ); + + bool guiEnabled = GUI.enabled; + GUI.enabled = false; + EditorGUILayout.ObjectField( string.Empty, subAssetsToSearch[j].subAsset, typeof( Object ), true ); + GUI.enabled = guiEnabled; + + GUILayout.EndHorizontal(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/ListDrawer.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/ListDrawer.cs.meta new file mode 100644 index 0000000..555a369 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/ListDrawer.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 88a4a4e861026b2498a437ce1e12b054 +timeCreated: 1568758673 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/ObjectToSearch.cs b/Assets/Plugins/AssetUsageDetector/Editor/ObjectToSearch.cs new file mode 100644 index 0000000..24ad8ac --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/ObjectToSearch.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace AssetUsageDetectorNamespace +{ + [Serializable] + public class ObjectToSearch + { + [Serializable] + public class SubAsset + { + public Object subAsset; + public bool shouldSearch; + + public SubAsset( Object subAsset, bool shouldSearch ) + { + this.subAsset = subAsset; + this.shouldSearch = shouldSearch; + } + } + + public Object obj; + public List subAssets; + public bool showSubAssetsFoldout; + + private static HashSet currentSubAssets; + + public ObjectToSearch( Object obj, bool? shouldSearchChildren = null ) + { + this.obj = obj; + RefreshSubAssets( shouldSearchChildren ); + } + + public void RefreshSubAssets( bool? shouldSearchChildren = null ) + { + if( subAssets == null ) + subAssets = new List(); + else + subAssets.Clear(); + + if( currentSubAssets == null ) + currentSubAssets = new HashSet(); + else + currentSubAssets.Clear(); + + AddSubAssets( obj, false, shouldSearchChildren ); + currentSubAssets.Clear(); + } + + private void AddSubAssets( Object target, bool includeTarget, bool? shouldSearchChildren ) + { + if( target == null || target.Equals( null ) ) + return; + + if( !target.IsAsset() ) + { + GameObject go = target as GameObject; + if( !go || !go.scene.IsValid() ) + return; + + // If this is a scene object, add its child objects to the sub-assets list + // but don't include them in the search by default + Transform goTransform = go.transform; + Transform[] children = go.GetComponentsInChildren( true ); + for( int i = 0; i < children.Length; i++ ) + { + if( ReferenceEquals( children[i], goTransform ) ) + continue; + + subAssets.Add( new SubAsset( children[i].gameObject, shouldSearchChildren ?? false ) ); + } + } + else + { + if( !AssetDatabase.IsMainAsset( target ) || target is SceneAsset ) + return; + + if( includeTarget ) + { + if( currentSubAssets.Add( target ) ) + subAssets.Add( new SubAsset( target, shouldSearchChildren ?? true ) ); + } + else + { + // If asset is a directory, add all of its contents as sub-assets recursively + if( target.IsFolder() ) + { + foreach( string filePath in Utilities.EnumerateFolderContents( target ) ) + AddSubAssets( AssetDatabase.LoadAssetAtPath( filePath ), true, shouldSearchChildren ); + + return; + } + } + + // Find sub-asset(s) of the asset (if any) + Object[] assets = AssetDatabase.LoadAllAssetsAtPath( AssetDatabase.GetAssetPath( target ) ); + for( int i = 0; i < assets.Length; i++ ) + { + Object asset = assets[i]; + if( asset == null || asset.Equals( null ) || asset is Component || asset == target ) + continue; + +#if UNITY_2018_3_OR_NEWER + // Nested prefabs in prefab assets add an additional native object of type 'UnityEngine.PrefabInstance' to the prefab. Managed type of that native type + // is UnityEngine.Object (i.e. GetType() returns UnityEngine.Object, not UnityEngine.PrefabInstance). There are no possible references to these native + // objects so skip them (we're checking for UnityEngine.Prefab because it includes other native types like UnityEngine.PrefabCreation, as well) + if( target is GameObject && asset.GetType() == typeof( Object ) && asset.ToString().Contains( "(UnityEngine.Prefab" ) ) + continue; +#endif + + if( currentSubAssets.Add( asset ) ) + subAssets.Add( new SubAsset( asset, shouldSearchChildren ?? true ) ); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/ObjectToSearch.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/ObjectToSearch.cs.meta new file mode 100644 index 0000000..787b336 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/ObjectToSearch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66d5a144a723fea40945afc069d4231d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchRefactoring.cs b/Assets/Plugins/AssetUsageDetector/Editor/SearchRefactoring.cs new file mode 100644 index 0000000..ea2bf99 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchRefactoring.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace AssetUsageDetectorNamespace +{ + public delegate void SearchRefactoring( SearchMatch match ); + + public abstract class SearchMatch + { + public readonly object Source; + public readonly Object Context; // Almost always equal to Source. This is the Object that needs to be dirtied (if not null) to notify Unity of changes to Value + public Object Value { get; private set; } + + protected SearchMatch( object source, Object value ) + { + Source = source; + Context = source as Object; + Value = value; + } + + protected SearchMatch( object source, Object value, Object context ) : this( source, value ) + { + Context = context; + } + + public void ChangeValue( Object newValue ) + { + if( newValue == Value ) + return; + + if( Context && ( Context.hideFlags & HideFlags.NotEditable ) == HideFlags.NotEditable ) + { + Debug.LogWarning( "Can't change value of read-only Object: " + Context, Context ); + return; + } + + try + { + bool setContextDirty; + if( ChangeValue( newValue, out setContextDirty ) ) + OnValueChanged( newValue, setContextDirty ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } + } + + protected abstract bool ChangeValue( Object newValue, out bool setContextDirty ); + + public void OnValueChanged( Object newValue, bool setContextDirty = true ) + { + Value = newValue; + + if( setContextDirty ) + { + if( Context ) + { + if( AssetDatabase.Contains( Context ) ) + EditorUtility.SetDirty( Context ); + else if( !EditorApplication.isPlaying ) + { + EditorUtility.SetDirty( Context ); + + if( Context is Component ) + EditorSceneManager.MarkSceneDirty( ( (Component) Context ).gameObject.scene ); + else if( Context is GameObject ) + EditorSceneManager.MarkSceneDirty( ( (GameObject) Context ).scene ); + else + EditorSceneManager.MarkAllScenesDirty(); + } + } + else if( !EditorApplication.isPlaying ) + EditorSceneManager.MarkAllScenesDirty(); + } + } + } + + public abstract class GenericSearchMatch : SearchMatch + { + public delegate void SetterFunction( Object newValue ); + + public readonly SetterFunction Setter; + + internal GenericSearchMatch( object source, Object value, SetterFunction setter ) : base( source, value ) { Setter = setter; } + internal GenericSearchMatch( object source, Object value, Object context, SetterFunction setter ) : base( source, value, context ) { Setter = setter; } + + protected override bool ChangeValue( Object newValue, out bool setContextDirty ) + { + Setter( newValue ); + + setContextDirty = true; + return true; + } + } + + public abstract class ReadOnlySearchMatch : SearchMatch + { + internal ReadOnlySearchMatch( object source, Object value ) : base( source, value ) { } + + protected override bool ChangeValue( Object newValue, out bool setContextDirty ) + { + Debug.LogWarning( "Can't change value of " + GetType().Name ); + + setContextDirty = false; + return false; + } + } + + /// + /// - Source: Object whose SerializedProperty points to Value + /// - Value: Referenced object + /// - SerializedProperty: The SerializedProperty that points to Value + /// + public class SerializedPropertyMatch : SearchMatch + { + public readonly SerializedProperty SerializedProperty; // Next or NextVisible mustn't be called with this SerializedProperty + + internal SerializedPropertyMatch( Object source, Object value, SerializedProperty property ) : base( source, value ) { SerializedProperty = property; } + + protected override bool ChangeValue( Object newValue, out bool setContextDirty ) + { + setContextDirty = true; + + switch( SerializedProperty.propertyType ) + { + case SerializedPropertyType.ObjectReference: + SerializedProperty.objectReferenceValue = newValue; + if( SerializedProperty.objectReferenceValue != newValue ) + { + Debug.LogWarning( "Couldn't cast " + newValue.GetType() + " to " + SerializedProperty.type ); + SerializedProperty.objectReferenceValue = Value; + + return false; + } + + break; + case SerializedPropertyType.ExposedReference: + SerializedProperty.exposedReferenceValue = newValue; + if( SerializedProperty.exposedReferenceValue != newValue ) + { + Debug.LogWarning( "Couldn't cast " + newValue.GetType() + " to " + SerializedProperty.type ); + SerializedProperty.exposedReferenceValue = Value; + + return false; + } + + break; +#if UNITY_2019_3_OR_NEWER + case SerializedPropertyType.ManagedReference: SerializedProperty.managedReferenceValue = newValue; break; +#endif + } + + SerializedProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo(); + return true; + } + } + + /// + /// - Source: Object whose variable points to Value + /// - Value: Referenced object + /// - Variable: FieldInfo, PropertyInfo or IEnumerable (ChangeValue may not work for all IEnumerables) + /// + public class ReflectionMatch : SearchMatch + { + public readonly object Variable; + + internal ReflectionMatch( object source, Object value, object variable ) : base( source, value ) { Variable = variable; } + + protected override bool ChangeValue( Object newValue, out bool setContextDirty ) + { + setContextDirty = true; + + if( Variable is FieldInfo ) + ( (FieldInfo) Variable ).SetValue( Source, newValue ); + else if( Variable is PropertyInfo ) + { + PropertyInfo property = (PropertyInfo) Variable; + if( !property.CanWrite ) + { + Debug.LogWarning( "Property is read-only: " + property.DeclaringType.FullName + "." + property.Name ); + return false; + } + + property.SetValue( Source, newValue, null ); + } + else if( Variable is IList ) + { + IList list = (IList) Variable; + for( int i = list.Count - 1; i >= 0; i-- ) + { + if( ReferenceEquals( list[i], Value ) ) + list[i] = newValue; + } + } + else if( Variable is IDictionary ) + { + IDictionary dictionary = (IDictionary) Variable; + bool dictionaryModified; + do + { + dictionaryModified = false; + foreach( object dictKey in dictionary.Keys ) + { + object dictValue = dictionary[dictKey]; + if( ReferenceEquals( dictKey, Value ) ) + { + dictionary.Remove( dictKey ); + if( newValue ) + dictionary[newValue] = dictValue; + + dictionaryModified = true; + break; + } + else if( ReferenceEquals( dictValue, Value ) ) + { + dictionary[dictKey] = newValue; + dictionaryModified = true; + break; + } + } + } while( dictionaryModified ); + } + else + { + Debug.LogWarning( "Can't change value of " + Variable.GetType().Name ); + return false; + } + + return true; + } + } + + /// + /// - Source: MonoImporter (for scripts) or ShaderImporter + /// - Value: Default value assigned to Source's specified variable in the Inspector + /// - Variable: The variable of Source that Value is assigned to as default value + /// - MonoScriptAllVariables: All variables of Source script if it's MonoImporter + /// + public class AssetImporterDefaultValueMatch : SearchMatch + { + public readonly string Variable; + public readonly VariableGetterHolder[] MonoScriptAllVariables; + + internal AssetImporterDefaultValueMatch( Object source, Object value, string variable, VariableGetterHolder[] monoScriptAllVariables ) : base( source, value ) + { + Variable = variable; + MonoScriptAllVariables = monoScriptAllVariables; + } + + protected override bool ChangeValue( Object newValue, out bool setContextDirty ) + { + setContextDirty = false; + + if( Source is MonoImporter ) + { + MonoImporter monoImporter = (MonoImporter) Source; + + List variableNames = new List( 8 ); + List variableValues = new List( 8 ); + + for( int i = 0; i < MonoScriptAllVariables.Length; i++ ) + { + if( MonoScriptAllVariables[i].isSerializable && !MonoScriptAllVariables[i].IsProperty ) + { + Object variableDefaultValue = monoImporter.GetDefaultReference( MonoScriptAllVariables[i].Name ); + if( variableDefaultValue == Value && MonoScriptAllVariables[i].Name == Variable ) + variableDefaultValue = newValue; + + variableNames.Add( MonoScriptAllVariables[i].Name ); + variableValues.Add( variableDefaultValue ); + } + } + + monoImporter.SetDefaultReferences( variableNames.ToArray(), variableValues.ToArray() ); + EditorApplication.delayCall += () => AssetDatabase.ImportAsset( monoImporter.assetPath ); // If code recompiles during search, it will break the search. Give it a 1 frame delay + } + else if( Source is ShaderImporter ) + { + ShaderImporter shaderImporter = (ShaderImporter) Source; + Shader shader = shaderImporter.GetShader(); + + List textureNames = new List( 16 ); + List textureValues = new List( 16 ); +#if UNITY_2018_1_OR_NEWER + List nonModifiableTextureNames = new List( 16 ); + List nonModifiableTextureValues = new List( 16 ); +#endif + + int shaderPropertyCount = ShaderUtil.GetPropertyCount( shader ); + for( int i = 0; i < shaderPropertyCount; i++ ) + { + if( ShaderUtil.GetPropertyType( shader, i ) != ShaderUtil.ShaderPropertyType.TexEnv ) + continue; + + string propertyName = ShaderUtil.GetPropertyName( shader, i ); +#if UNITY_2018_1_OR_NEWER + if( ShaderUtil.IsShaderPropertyNonModifiableTexureProperty( shader, i ) ) + { + Texture propertyDefaultValue = shaderImporter.GetNonModifiableTexture( propertyName ); + if( propertyDefaultValue == Value && propertyName == Variable ) + propertyDefaultValue = (Texture) newValue; + + nonModifiableTextureNames.Add( propertyName ); + nonModifiableTextureValues.Add( propertyDefaultValue ); + } + else +#endif + { + Texture propertyDefaultValue = shaderImporter.GetDefaultTexture( propertyName ); + if( propertyDefaultValue == Value && propertyName == Variable ) + propertyDefaultValue = (Texture) newValue; + + textureNames.Add( propertyName ); + textureValues.Add( propertyDefaultValue ); + } + } + + shaderImporter.SetDefaultTextures( textureNames.ToArray(), textureValues.ToArray() ); +#if UNITY_2018_1_OR_NEWER + shaderImporter.SetNonModifiableTextures( nonModifiableTextureNames.ToArray(), nonModifiableTextureValues.ToArray() ); +#endif + AssetDatabase.ImportAsset( shaderImporter.assetPath ); + } + else + { + Debug.LogWarning( "Can't change default value of: " + Source.GetType() ); + return false; + } + + return true; + } + } + + /// + /// - Source: Animation, Animator, AnimatorStateMachine, AnimatorState, AnimatorControllerLayer, BlendTree, PlayableDirector* or AnimationClip* + /// - Context: If Source is AnimatorControllerLayer, then its RuntimeAnimatorController. Otherwise, equal to Source + /// - Value: AnimationClip, AnimatorController or AvatarMask used in Source (*for PlayableDirector and AnimationClip, it can be any Object value) + /// + public class AnimationSystemMatch : GenericSearchMatch + { + internal AnimationSystemMatch( object source, Object value, SetterFunction setter ) : base( source, value, setter ) { } + internal AnimationSystemMatch( object source, Object value, Object context, SetterFunction setter ) : base( source, value, context, setter ) { } + } + + /// + /// - Source: GameObject, AnimatorStateMachine or AnimatorState + /// - Value: The attached behaviour's source script (C# script or DLL, i.e. MonoScript) + /// - Behaviour: The attached behaviour (MonoBehaviour or StateMachineBehaviour) + /// + public class BehaviourUsageMatch : ReadOnlySearchMatch + { + public readonly Object Behaviour; + + internal BehaviourUsageMatch( Object source, MonoScript value, Object behaviour ) : base( source, value ) { Behaviour = behaviour; } + } + + /// + /// - Source: GameObject Instance + /// - Value: Prefab of that GameObject + /// + public class PrefabMatch : ReadOnlySearchMatch + { + internal PrefabMatch( Object source, Object value ) : base( source, value ) { } + } + + /// + /// - Source: Object that references Value + /// - Value: Matched object + /// + public class OtherSearchMatch : GenericSearchMatch + { + internal OtherSearchMatch( object source, Object value, SetterFunction setter ) : base( source, value, setter ) { } + internal OtherSearchMatch( object source, Object value, Object context, SetterFunction setter ) : base( source, value, context, setter ) { } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchRefactoring.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/SearchRefactoring.cs.meta new file mode 100644 index 0000000..c586f0c --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchRefactoring.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 3d15ef8bd8f7c7c4e8d228c99713b7eb +timeCreated: 1641132238 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchResult.cs b/Assets/Plugins/AssetUsageDetector/Editor/SearchResult.cs new file mode 100644 index 0000000..e9b6641 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchResult.cs @@ -0,0 +1,1395 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEditor.IMGUI.Controls; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; + +namespace AssetUsageDetectorNamespace +{ + // Custom class to hold search results + [Serializable] + public class SearchResult : IEnumerable, ISerializationCallbackReceiver + { + [Serializable] + internal class SerializableResultGroup + { + public string title; + public SearchResultGroup.GroupType type; + public bool isExpanded; + public bool pendingSearch; + public SearchResultTreeViewState treeViewState; + + public List initialSerializedNodes; + } + + [Serializable] + internal class SerializableNode + { + [Serializable] + public class SerializableLinkDescriptions + { + public List value; + } + + public string label; + public int instanceId; + public bool isUnityObject, isMainReference; + public ReferenceNode.UsedState usedState; + + public List links; + public List linkDescriptions; + public List linkWeakStates; + } + + internal class SortedEntry : IComparable + { + public readonly string assetPath, subAssetName; + public readonly bool isMainAsset; + public readonly Transform transform; + public readonly object entry; + + public SortedEntry( ReferenceNode node ) : this( node.nodeObject as Object ) + { + entry = node; + } + + public SortedEntry( ReferenceNode.Link link ) : this( link.targetNode.nodeObject as Object ) + { + entry = link; + } + + private SortedEntry( Object obj ) + { + if( obj ) + { + assetPath = AssetDatabase.GetAssetPath( obj ); + if( string.IsNullOrEmpty( assetPath ) ) + { + assetPath = null; + + if( obj is Component ) + transform = ( (Component) obj ).transform; + else if( obj is GameObject ) + transform = ( (GameObject) obj ).transform; + } + else + { + isMainAsset = AssetDatabase.IsMainAsset( ( obj is Component ) ? ( (Component) obj ).gameObject : obj ); + if( !isMainAsset ) + subAssetName = obj.name; + } + } + } + + // Sorting order: + // 1) Scene objects come first and are sorted by their absolute sibling indices in Hierarchy + // 2) Assets come later and are sorted by their asset paths (for assets sharing the same path, main assets come first and sub-assets are then sorted by their names) + // 3) Regular C# objects come last + int IComparable.CompareTo( SortedEntry other ) + { + if( this == other ) + return 0; + else if( assetPath == null ) + { + if( !transform ) + return 1; + else if( other.assetPath == null ) + return other.transform ? Utilities.CompareHierarchySiblingIndices( transform, other.transform ) : -1; + else + return -1; + } + else if( other.assetPath == null ) + return other.transform ? 1 : -1; + else + { + int assetPathComparison = EditorUtility.NaturalCompare( assetPath, other.assetPath ); + if( assetPathComparison != 0 ) + return assetPathComparison; + else if( isMainAsset ) + return -1; + else if( other.isMainAsset ) + return 1; + else + return subAssetName.CompareTo( other.subAssetName ); + } + } + } + + private bool success; // This isn't readonly so that it can be serialized + + private List result; + private SceneSetup[] initialSceneSetup; + + private AssetUsageDetector searchHandler; + private AssetUsageDetector.Parameters m_searchParameters; + + // Each TreeView in the drawn search results must use unique ids for their TreeViewItems. Otherwise, strange things happen: https://forum.unity.com/threads/multiple-editor-treeviews-selection-issue.601471/ + internal int nextTreeViewId = 1; + + private List serializedNodes; + private List serializedGroups; + private Object[] serializedUsedObjects; + + public int NumberOfGroups { get { return result.Count; } } + public SearchResultGroup this[int index] { get { return result[index]; } } + + public HashSet UsedObjects { get; private set; } + + public bool SearchCompletedSuccessfully { get { return success; } } + public bool InitialSceneSetupConfigured { get { return initialSceneSetup != null && initialSceneSetup.Length > 0; } } + public bool HasPendingLazySceneSearchResults { get { return result.Find( ( group ) => group.PendingSearch ) != null; } } + public AssetUsageDetector.Parameters SearchParameters { get { return m_searchParameters; } } + + public SearchResult( bool success, List result, HashSet usedObjects, SceneSetup[] initialSceneSetup, AssetUsageDetector searchHandler, AssetUsageDetector.Parameters searchParameters ) + { + if( result == null ) + result = new List( 0 ); + + this.success = success; + this.result = result; + this.UsedObjects = usedObjects ?? new HashSet(); + this.initialSceneSetup = initialSceneSetup; + this.searchHandler = searchHandler; + this.m_searchParameters = searchParameters; + } + + public void RemoveSearchResultGroup( SearchResultGroup searchResultGroup ) + { + result.Remove( searchResultGroup ); + } + + public void RefreshSearchResultGroup( SearchResultGroup searchResultGroup, bool noAssetDatabaseChanges ) + { + if( searchResultGroup == null ) + { + Debug.LogError( "SearchResultGroup is null!" ); + return; + } + + int searchResultGroupIndex = result.IndexOf( searchResultGroup ); + if( searchResultGroupIndex < 0 ) + { + Debug.LogError( "SearchResultGroup is not a part of SearchResult!" ); + return; + } + + if( searchResultGroup.Type == SearchResultGroup.GroupType.Scene && EditorApplication.isPlaying && !EditorSceneManager.GetSceneByPath( searchResultGroup.ScenePath ).isLoaded ) + { + Debug.LogError( "Can't search unloaded scene while in Play Mode!" ); + return; + } + + if( searchHandler == null ) + searchHandler = new AssetUsageDetector(); + + SceneSearchMode searchInScenes = m_searchParameters.searchInScenes; + Object[] searchInScenesSubset = m_searchParameters.searchInScenesSubset; + bool searchInAssetsFolder = m_searchParameters.searchInAssetsFolder; + Object[] searchInAssetsSubset = m_searchParameters.searchInAssetsSubset; + bool searchInProjectSettings = m_searchParameters.searchInProjectSettings; + bool lazySceneSearch = m_searchParameters.lazySceneSearch; + bool calculateUnusedObjects = m_searchParameters.calculateUnusedObjects; + bool _noAssetDatabaseChanges = m_searchParameters.noAssetDatabaseChanges; + + try + { + if( searchResultGroup.Type == SearchResultGroup.GroupType.Assets ) + { + m_searchParameters.searchInScenes = SceneSearchMode.None; + m_searchParameters.searchInScenesSubset = null; + m_searchParameters.searchInProjectSettings = false; + } + else if( searchResultGroup.Type == SearchResultGroup.GroupType.ProjectSettings ) + { + m_searchParameters.searchInScenes = SceneSearchMode.None; + m_searchParameters.searchInScenesSubset = null; + m_searchParameters.searchInAssetsFolder = false; + m_searchParameters.searchInAssetsSubset = null; + m_searchParameters.searchInProjectSettings = true; + } + else if( searchResultGroup.Type == SearchResultGroup.GroupType.Scene ) + { + m_searchParameters.searchInScenes = SceneSearchMode.None; + m_searchParameters.searchInScenesSubset = new Object[1] { AssetDatabase.LoadAssetAtPath( searchResultGroup.ScenePath ) }; + m_searchParameters.searchInAssetsFolder = false; + m_searchParameters.searchInAssetsSubset = null; + m_searchParameters.searchInProjectSettings = false; + } + else if( searchResultGroup.Type == SearchResultGroup.GroupType.DontDestroyOnLoad ) + { + m_searchParameters.searchInScenes = (SceneSearchMode) 1024; // A unique value to search only the DontDestroyOnLoad scene + m_searchParameters.searchInScenesSubset = null; + m_searchParameters.searchInAssetsFolder = false; + m_searchParameters.searchInAssetsSubset = null; + m_searchParameters.searchInProjectSettings = false; + } + else + { + Debug.LogError( "Can't refresh group: " + searchResultGroup.Type ); + return; + } + + m_searchParameters.lazySceneSearch = false; + m_searchParameters.calculateUnusedObjects = result.Find( ( group ) => group.Type == SearchResultGroup.GroupType.UnusedObjects ) != null; + m_searchParameters.noAssetDatabaseChanges = noAssetDatabaseChanges; + + // Make sure the AssetDatabase is up-to-date + AssetDatabase.SaveAssets(); + + SearchResult searchResult = searchHandler.Run( m_searchParameters ); + if( !searchResult.success ) + { + EditorUtility.DisplayDialog( "Error", "Couldn't refresh, check console for more info.", "OK" ); + return; + } + + if( searchResult.result != null ) + { + SearchResultGroup newSearchResultGroup = searchResult.result.Find( ( group ) => group.Title == searchResultGroup.Title ); + if( newSearchResultGroup != null ) + result[searchResultGroupIndex] = newSearchResultGroup; + else + searchResultGroup.Clear(); + + UsedObjects.UnionWith( searchResult.UsedObjects ); + + SearchResultGroup unusedObjectsSearchResultGroup = result.Find( ( group ) => group.Type == SearchResultGroup.GroupType.UnusedObjects ); + if( unusedObjectsSearchResultGroup != null ) + { + SearchResultGroup newUnusedObjectsSearchResultGroup = searchResult.result.Find( ( group ) => group.Type == SearchResultGroup.GroupType.UnusedObjects ); + if( newUnusedObjectsSearchResultGroup == null ) + { + // UnusedObjects search result group doesn't exist in 2 cases: + // - When there are no search results found (NumberOfGroups == 0) + // - When all searched objects are referenced (NumberOfGroups > 0) + if( searchResult.result.Count > 0 ) + unusedObjectsSearchResultGroup.Clear(); + } + else + { + // NOTE: We can process UnusedObjects graphs iteratively (instead of recursively) because for the time being, these graphs have a maximum depth of 1 + bool unusedObjectsGraphChanged = false; + HashSet newUnusedObjectsSet = new HashSet(); + for( int i = newUnusedObjectsSearchResultGroup.NumberOfReferences - 1; i >= 0; i-- ) + { + ReferenceNode node = newUnusedObjectsSearchResultGroup[i]; + newUnusedObjectsSet.Add( node.UnityObject ); + + for( int j = node.NumberOfOutgoingLinks - 1; j >= 0; j-- ) + newUnusedObjectsSet.Add( node[j].targetNode.UnityObject ); + } + + for( int i = unusedObjectsSearchResultGroup.NumberOfReferences - 1; i >= 0; i-- ) + { + ReferenceNode node = unusedObjectsSearchResultGroup[i]; + bool parentNodeRemoved = false; + Object obj = node.UnityObject; + if( !obj || !newUnusedObjectsSet.Contains( obj ) ) + { + unusedObjectsSearchResultGroup.RemoveReference( i ); + unusedObjectsGraphChanged = parentNodeRemoved = true; + } + + bool hasUnusedSubObjects = false, hasUsedSubObjects = false; + bool hadSubObjects = node.NumberOfOutgoingLinks > 0; + for( int j = 0; j < node.NumberOfOutgoingLinks; j++ ) + { + if( node[j].targetNode.usedState == ReferenceNode.UsedState.Used ) // User has explicitly displayed this used child object/sub-asset in the TreeView + continue; + + Object _obj = node[j].targetNode.UnityObject; + if( newUnusedObjectsSet.Contains( _obj ) ) + { + hasUnusedSubObjects = true; + + if( parentNodeRemoved ) + unusedObjectsSearchResultGroup.AddReference( node[j].targetNode ); + } + else if( !parentNodeRemoved ) + { + node.RemoveLink( j-- ); + unusedObjectsGraphChanged = hasUsedSubObjects = true; + } + } + + if( !parentNodeRemoved ) + { + // When all sub-assets of a main asset are used, consider the main asset as used, as well + if( !hasUnusedSubObjects && hadSubObjects && AssetDatabase.IsMainAsset( obj ) ) + { + unusedObjectsSearchResultGroup.RemoveReference( i ); + unusedObjectsGraphChanged = true; + } + + if( hasUsedSubObjects && node.usedState == ReferenceNode.UsedState.Unused ) + node.usedState = ReferenceNode.UsedState.MixedCollapsed; + } + } + + if( unusedObjectsGraphChanged && unusedObjectsSearchResultGroup.treeView != null ) + { + unusedObjectsSearchResultGroup.treeView.SetSelection( new int[0], TreeViewSelectionOptions.FireSelectionChanged ); + unusedObjectsSearchResultGroup.treeViewState.preSearchExpandedIds = null; + unusedObjectsSearchResultGroup.treeView.Reload(); + unusedObjectsSearchResultGroup.treeView.ExpandAll(); + } + } + + if( unusedObjectsSearchResultGroup.NumberOfReferences == 0 ) + result.Remove( unusedObjectsSearchResultGroup ); + } + } + } + finally + { + m_searchParameters.searchInScenes = searchInScenes; + m_searchParameters.searchInScenesSubset = searchInScenesSubset; + m_searchParameters.searchInAssetsFolder = searchInAssetsFolder; + m_searchParameters.searchInAssetsSubset = searchInAssetsSubset; + m_searchParameters.searchInProjectSettings = searchInProjectSettings; + m_searchParameters.lazySceneSearch = lazySceneSearch; + m_searchParameters.calculateUnusedObjects = calculateUnusedObjects; + m_searchParameters.noAssetDatabaseChanges = _noAssetDatabaseChanges; + } + } + + public float DrawOnGUI( EditorWindow window, float scrollPosition, bool noAssetDatabaseChanges ) + { + for( int i = 0; i < result.Count; i++ ) + { + scrollPosition = result[i].DrawOnGUI( this, window, scrollPosition, noAssetDatabaseChanges ); + + if( i < result.Count - 1 ) + GUILayout.Space( 10f ); + } + + return scrollPosition; + } + + public int IndexOf( SearchResultGroup searchResultGroup ) + { + return result.IndexOf( searchResultGroup ); + } + + public void CollapseAllSearchResultGroups() + { + for( int i = 0; i < result.Count; i++ ) + result[i].Collapse(); + } + + public void CancelDelayedTreeViewTooltip() + { + for( int i = 0; i < result.Count; i++ ) + { + if( result[i].treeView != null ) + result[i].treeView.CancelDelayedTooltip(); + } + } + + // Returns if RestoreInitialSceneSetup will have any effect on the current scene setup + public bool IsSceneSetupDifferentThanCurrentSetup() + { + if( initialSceneSetup == null ) + return false; + + SceneSetup[] sceneFinalSetup = EditorSceneManager.GetSceneManagerSetup(); + if( initialSceneSetup.Length != sceneFinalSetup.Length ) + return true; + + for( int i = 0; i < sceneFinalSetup.Length; i++ ) + { + bool sceneIsOneOfInitials = false; + for( int j = 0; j < initialSceneSetup.Length; j++ ) + { + if( sceneFinalSetup[i].path == initialSceneSetup[j].path ) + { + if( sceneFinalSetup[i].isLoaded != initialSceneSetup[j].isLoaded ) + return true; + + sceneIsOneOfInitials = true; + break; + } + } + + if( !sceneIsOneOfInitials ) + return true; + } + + return false; + } + + // Close the scenes that were not part of the initial scene setup + // Returns true if initial scene setup is restored successfully + public bool RestoreInitialSceneSetup() + { + if( initialSceneSetup == null || initialSceneSetup.Length == 0 ) + return true; + + if( EditorApplication.isPlaying ) + return false; + + if( !IsSceneSetupDifferentThanCurrentSetup() ) + return true; + + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + + sb.AppendLine( "Restore initial scene setup?" ); + for( int i = 0; i < initialSceneSetup.Length; i++ ) + sb.AppendLine().Append( "- " ).Append( initialSceneSetup[i].path ); + + switch( EditorUtility.DisplayDialogComplex( "Asset Usage Detector", sb.ToString(), "Yes", "Cancel", "Leave it as is" ) ) + { + case 1: return false; + case 2: return true; + } + + if( !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo() ) + return false; + + for( int i = 0; i < initialSceneSetup.Length; i++ ) + { + Scene scene = EditorSceneManager.GetSceneByPath( initialSceneSetup[i].path ); + if( !scene.isLoaded ) + scene = EditorSceneManager.OpenScene( initialSceneSetup[i].path, initialSceneSetup[i].isLoaded ? OpenSceneMode.Additive : OpenSceneMode.AdditiveWithoutLoading ); + + if( initialSceneSetup[i].isActive ) + EditorSceneManager.SetActiveScene( scene ); + } + + SceneSetup[] sceneFinalSetup = EditorSceneManager.GetSceneManagerSetup(); + for( int i = 0; i < sceneFinalSetup.Length; i++ ) + { + bool sceneIsOneOfInitials = false; + for( int j = 0; j < initialSceneSetup.Length; j++ ) + { + if( sceneFinalSetup[i].path == initialSceneSetup[j].path ) + { + sceneIsOneOfInitials = true; + break; + } + } + + if( !sceneIsOneOfInitials ) + EditorSceneManager.CloseScene( EditorSceneManager.GetSceneByPath( sceneFinalSetup[i].path ), true ); + } + + for( int i = 0; i < initialSceneSetup.Length; i++ ) + { + if( !initialSceneSetup[i].isLoaded ) + EditorSceneManager.CloseScene( EditorSceneManager.GetSceneByPath( initialSceneSetup[i].path ), false ); + } + + initialSceneSetup = null; + return true; + } + + // Assembly reloading; serialize nodes in a way that Unity can serialize + // Credit: https://docs.unity3d.com/Manual/script-Serialization-Custom.html + void ISerializationCallbackReceiver.OnBeforeSerialize() + { + if( result == null ) + return; + + if( serializedGroups == null ) + serializedGroups = new List( result.Count ); + else + serializedGroups.Clear(); + + if( serializedNodes == null ) + serializedNodes = new List( result.Count * 16 ); + else + serializedNodes.Clear(); + + Dictionary nodeToIndex = new Dictionary( result.Count * 16 ); + for( int i = 0; i < result.Count; i++ ) + serializedGroups.Add( result[i].Serialize( nodeToIndex, serializedNodes ) ); + + if( serializedUsedObjects == null || serializedUsedObjects.Length != UsedObjects.Count ) + serializedUsedObjects = new Object[UsedObjects.Count]; + + UsedObjects.CopyTo( serializedUsedObjects ); + } + + // Assembly reloaded; deserialize nodes to construct the original graph + void ISerializationCallbackReceiver.OnAfterDeserialize() + { + if( serializedGroups == null || serializedNodes == null || serializedUsedObjects == null ) + return; + + if( result == null ) + result = new List( serializedGroups.Count ); + else + result.Clear(); + + List allNodes = new List( serializedNodes.Count ); + for( int i = 0; i < serializedNodes.Count; i++ ) + allNodes.Add( new ReferenceNode() ); + + for( int i = 0; i < serializedNodes.Count; i++ ) + allNodes[i].Deserialize( serializedNodes[i], allNodes ); + + for( int i = 0; i < serializedGroups.Count; i++ ) + { + result.Add( new SearchResultGroup( serializedGroups[i].title, serializedGroups[i].type, serializedGroups[i].isExpanded, serializedGroups[i].pendingSearch ) ); + result[i].Deserialize( serializedGroups[i], allNodes ); + } + + if( UsedObjects == null ) + UsedObjects = new HashSet( serializedUsedObjects ); + else + UsedObjects.UnionWith( serializedUsedObjects ); + + serializedNodes.Clear(); + serializedGroups.Clear(); + serializedUsedObjects = null; + } + + IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) result ).GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) result ).GetEnumerator(); } + } + + // Custom class to hold the results for a single scene or Assets folder + public class SearchResultGroup : IEnumerable + { + public enum GroupType { Assets = 0, Scene = 1, DontDestroyOnLoad = 2, ProjectSettings = 3, UnusedObjects = 4 }; + + private readonly List references = new List(); + + internal SearchResultTreeView treeView; + internal SearchResultTreeViewState treeViewState; + private Rect lastTreeViewRect; + private SearchField treeViewSearchField; + + public string Title { get; private set; } + public GroupType Type { get; private set; } + public string ScenePath { get; private set; } + public bool IsExpanded { get; private set; } + public bool PendingSearch { get; private set; } + + public int NumberOfReferences { get { return references.Count; } } + public ReferenceNode this[int index] { get { return references[index]; } } + + public SearchResultGroup( string title, GroupType type, bool isExpanded = true, bool pendingSearch = false ) + { + Title = title.StartsWith( "" ) ? title : string.Concat( "", title, "" ); + ScenePath = type != GroupType.Scene ? null : ( title.StartsWith( "" ) ? title.Substring( 3, title.Length - 7 ) : title ); + Type = type; + IsExpanded = isExpanded; + PendingSearch = pendingSearch; + } + + public void AddReference( ReferenceNode node ) + { + references.Add( node ); + } + + public void RemoveReference( int index ) + { + references.RemoveAt( index ); + } + + // Removes all nodes + public void Clear() + { + PendingSearch = false; + + references.Clear(); + + treeView = null; + treeViewState = null; + treeViewSearchField = null; + } + + public void Collapse() + { + IsExpanded = false; + } + + // Initializes commonly used variables of the nodes + public void InitializeNodes( HashSet objectsToSearchSet ) + { + List _references = new List( references ); + references.Clear(); + + // Reverse the links of the search results graph so that the root ReferenceNodes are the searched objects + Dictionary reverseGraphNodes = new Dictionary( references.Count * 16 ); + for( int i = 0; i < _references.Count; i++ ) + _references[i].CreateReverseGraphRecursively( this, references, reverseGraphNodes, objectsToSearchSet ); + + // Remove weak links if they aren't ultimately connected to a non-weak link + HashSet visitedNodes = new HashSet(); + for( int i = references.Count - 1; i >= 0; i-- ) + references[i].RemoveRedundantLinksRecursively( visitedNodes ); + + // When a GameObject is a root node, then any components of that GameObject that are also root nodes should omit their links to the + // GameObject's node because otherwise, search results are filled with redundant 'GameObject->Its Component' references + HashSet rootGameObjectNodes = new HashSet(); + for( int i = references.Count - 1; i >= 0; i-- ) + { + if( references[i].nodeObject as GameObject ) + rootGameObjectNodes.Add( references[i] ); + } + + for( int i = references.Count - 1; i >= 0; i-- ) + { + ReferenceNode node = references[i]; + Component component = node.nodeObject as Component; + if( component ) + { + for( int j = node.NumberOfOutgoingLinks - 1; j >= 0; j-- ) + { + if( ReferenceEquals( node[j].targetNode.nodeObject, component.gameObject ) && rootGameObjectNodes.Contains( node[j].targetNode ) ) + { + node.RemoveLink( j ); + break; + } + } + } + } + + // Remove root nodes that don't have any outgoing links + for( int i = references.Count - 1; i >= 0; i-- ) + { + if( references[i].NumberOfOutgoingLinks == 0 ) + references.RemoveAt( i ); + } + + // Sort root nodes + if( references.Count > 1 ) + { + SearchResult.SortedEntry[] sortedEntries = new SearchResult.SortedEntry[references.Count]; + for( int i = references.Count - 1; i >= 0; i-- ) + sortedEntries[i] = new SearchResult.SortedEntry( references[i] ); + + Array.Sort( sortedEntries ); + + for( int i = 0; i < sortedEntries.Length; i++ ) + references[i] = (ReferenceNode) sortedEntries[i].entry; + } + + for( int i = references.Count - 1; i >= 0; i-- ) + { + references[i].SortLinks(); // Sort immediate links of the root nodes + references[i].InitializeRecursively(); + } + } + + // Draw the results found for this container + public float DrawOnGUI( SearchResult searchResult, EditorWindow window, float scrollPosition, bool noAssetDatabaseChanges ) + { + Event ev = Event.current; + Color c = GUI.backgroundColor; + + float headerHeight = EditorGUIUtility.singleLineHeight * 2f; + float refreshButtonWidth = 100f; + + GUI.backgroundColor = AssetUsageDetectorSettings.SearchResultGroupHeaderColor; + + Rect headerRect = EditorGUILayout.GetControlRect( false, headerHeight ); + float width = headerRect.width; + headerRect.width = headerHeight; + if( GUI.Button( headerRect, IsExpanded ? "v" : ">" ) ) + { + IsExpanded = !IsExpanded; + if( ev.alt && treeView != null ) + { + if( !IsExpanded ) + treeView.CollapseAll(); + else + treeView.ExpandAll(); + } + + window.Repaint(); + GUIUtility.ExitGUI(); + } + + headerRect.x += headerHeight; + headerRect.width = width - ( searchResult != null ? ( refreshButtonWidth + headerHeight ) : headerHeight ); + + if( GUI.Button( headerRect, Title, Utilities.BoxGUIStyle ) ) + { + if( ev.button != 1 ) + { + if( Type == GroupType.Scene ) + { + // If the container (scene, usually) is left clicked, highlight it on Project view + SceneAsset sceneAsset = AssetDatabase.LoadAssetAtPath( ScenePath ); + if( sceneAsset ) + { + if( AssetUsageDetectorSettings.PingClickedObjects ) + EditorGUIUtility.PingObject( sceneAsset ); + if( AssetUsageDetectorSettings.SelectClickedObjects ) + Selection.activeObject = sceneAsset; + } + } + } + else + { + GenericMenu contextMenu = new GenericMenu(); + + if( searchResult != null ) + contextMenu.AddItem( new GUIContent( "Hide" ), false, () => searchResult.RemoveSearchResultGroup( this ) ); + + if( references.Count > 0 && treeView != null ) + { + if( contextMenu.GetItemCount() > 0 ) + contextMenu.AddSeparator( "" ); + + if( Type != GroupType.UnusedObjects ) + { + contextMenu.AddItem( new GUIContent( "Expand Direct References Only" ), false, () => + { + treeView.ExpandDirectReferences(); + IsExpanded = true; + } ); + + contextMenu.AddItem( new GUIContent( "Expand Main References Only" ), false, () => + { + treeView.ExpandMainReferences(); + IsExpanded = true; + } ); + } + + if( !string.IsNullOrEmpty( treeViewState.searchTerm ) && treeViewState.searchMode == SearchResultTreeView.SearchMode.ReferencesOnly ) + { + contextMenu.AddItem( new GUIContent( "Expand Matching Search Results Only" ), false, () => + { + treeView.ExpandMatchingSearchResults(); + IsExpanded = true; + } ); + } + + contextMenu.AddItem( new GUIContent( "Expand All" ), false, () => + { + treeView.ExpandAll(); + IsExpanded = true; + } ); + + contextMenu.AddItem( new GUIContent( "Collapse All" ), false, () => + { + treeView.CollapseAll(); + IsExpanded = true; + } ); + + if( searchResult != null && searchResult.NumberOfGroups > 1 && !string.IsNullOrEmpty( treeViewState.searchTerm ) ) + { + if( contextMenu.GetItemCount() > 0 ) + contextMenu.AddSeparator( "" ); + + contextMenu.AddItem( new GUIContent( "Apply Search to All Results" ), false, () => + { + for( int i = 0; i < searchResult.NumberOfGroups; i++ ) + { + if( searchResult[i].treeView == null ) + continue; + + string previousSearchTerm = searchResult[i].treeViewState.searchTerm ?? ""; + SearchResultTreeView.SearchMode previousSearchMode = searchResult[i].treeViewState.searchMode; + + searchResult[i].treeViewState.searchTerm = treeViewState.searchTerm ?? ""; + searchResult[i].treeViewState.searchMode = treeViewState.searchMode; + + if( treeViewState.searchTerm != previousSearchTerm || treeViewState.searchMode != previousSearchMode ) + searchResult[i].treeView.RefreshSearch( previousSearchTerm ); + } + } ); + } + } + +#if UNITY_2022_2_OR_NEWER + int loadedSceneCount = SceneManager.loadedSceneCount; +#else + int loadedSceneCount = EditorSceneManager.loadedSceneCount; +#endif + if( Type == GroupType.Scene && !EditorApplication.isPlaying && loadedSceneCount > 1 ) + { + // Show context menu when SearchResultGroup's header is right clicked + Scene scene = EditorSceneManager.GetSceneByPath( ScenePath ); + if( scene.isLoaded ) + { + if( contextMenu.GetItemCount() > 0 ) + contextMenu.AddSeparator( "" ); + + contextMenu.AddItem( new GUIContent( "Close Scene" ), false, () => + { + if( !scene.isDirty || EditorSceneManager.SaveModifiedScenesIfUserWantsTo( new Scene[1] { scene } ) ) + EditorSceneManager.CloseScene( scene, true ); + } ); + } + } + + contextMenu.ShowAsContext(); + } + } + + if( searchResult != null ) + { + bool guiEnabled = GUI.enabled; + GUI.enabled = Type != GroupType.UnusedObjects; + + headerRect.x += width - ( refreshButtonWidth + headerHeight ); + headerRect.width = refreshButtonWidth; + if( GUI.Button( headerRect, "Refresh" ) ) + { + searchResult.RefreshSearchResultGroup( this, noAssetDatabaseChanges ); + GUIUtility.ExitGUI(); + } + + GUI.enabled = guiEnabled; + } + + GUI.backgroundColor = c; + + if( IsExpanded ) + { + if( PendingSearch ) + GUILayout.Box( "Lazy Search: this scene potentially has some references, hit Refresh to find them", Utilities.BoxGUIStyle ); + else if( references.Count == 0 ) + GUILayout.Box( ( Type == GroupType.UnusedObjects ) ? "No unused objects left..." : "No references found...", Utilities.BoxGUIStyle ); + else + { + if( Type == GroupType.UnusedObjects ) + { + if( searchResult != null && searchResult.HasPendingLazySceneSearchResults ) + EditorGUILayout.HelpBox( "Some scene(s) aren't searched yet (lazy scene search). Refreshing those scene(s) will automatically update this list.", MessageType.Warning ); + + if( searchResult != null && searchResult.SearchParameters.dontSearchInSourceAssets && searchResult.SearchParameters.objectsToSearch.Length > 1 ) + EditorGUILayout.HelpBox( "'Don't search \"SEARCHED OBJECTS\" themselves for references' is enabled, some of these objects might be used by \"SEARCHED OBJECTS\".", MessageType.Warning ); + + if( !AssetUsageDetectorSettings.MarkUsedAssetsSubAssetsAsUsed ) + EditorGUILayout.HelpBox( "'Hide unused sub-assets in \"Unused Objects\" list if their parent assets are used' is disabled, unused sub-assets' parent assets might be used.", MessageType.Warning ); + + EditorGUILayout.HelpBox( "Although no references to these objects are found, they might still be used somewhere (e.g. via Resources.Load). If you intend to delete these objects, consider creating a backup of your project first.", MessageType.Info ); + } + + if( treeView == null ) + { + bool isFirstInitialization = ( treeViewState == null ); + if( isFirstInitialization ) + treeViewState = new SearchResultTreeViewState(); + + // This isn't inside isFirstInitialization because SearchResultTreeViewState might have been initialized by + // Unity's serialization system after a domain reload + bool shouldUpdateInitialTreeViewNodeId = ( treeViewState.initialNodeId == 0 && searchResult != null ); + if( shouldUpdateInitialTreeViewNodeId ) + treeViewState.initialNodeId = searchResult.nextTreeViewId; + + treeView = new SearchResultTreeView( treeViewState, references, ( Type == GroupType.UnusedObjects ) ? SearchResultTreeView.TreeType.UnusedObjects : SearchResultTreeView.TreeType.Normal, searchResult != null ? searchResult.UsedObjects : null, searchResult != null && searchResult.SearchParameters.hideDuplicateRows, searchResult != null && searchResult.SearchParameters.hideReduntantPrefabVariantLinks, true ); + + if( isFirstInitialization ) + { + if( Type != GroupType.UnusedObjects ) + treeView.ExpandMainReferences(); + else + treeView.ExpandAll(); + } + + if( shouldUpdateInitialTreeViewNodeId ) + searchResult.nextTreeViewId = treeViewState.finalNodeId; + } + + if( treeViewSearchField == null ) + { + treeViewSearchField = new SearchField() { autoSetFocusOnFindCommand = false }; + treeViewSearchField.downOrUpArrowKeyPressed += () => treeView.SetFocusAndEnsureSelectedItem(); // Not assigning SetFocusAndEnsureSelectedItem directly in case treeView's value changes + } + + Rect searchFieldRect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight ); + string previousSearchTerm = treeViewState.searchTerm ?? ""; + SearchResultTreeView.SearchMode previousSearchMode = treeViewState.searchMode; + treeViewState.searchTerm = treeViewSearchField.OnToolbarGUI( new Rect( searchFieldRect.x, searchFieldRect.y, searchFieldRect.width - 100f, searchFieldRect.height ), treeViewState.searchTerm ) ?? ""; + treeViewState.searchMode = (SearchResultTreeView.SearchMode) EditorGUI.EnumPopup( new Rect( searchFieldRect.xMax - 100f, searchFieldRect.y, 100f, searchFieldRect.height ), treeViewState.searchMode ); + if( treeViewState.searchTerm != previousSearchTerm || treeViewState.searchMode != previousSearchMode ) + treeView.RefreshSearch( previousSearchTerm ); + + KeyCode pressedKeyboardNavigationKey = KeyCode.None; + bool treeViewKeyboardNavigation = false; + if( ev.type == EventType.KeyDown ) + { + pressedKeyboardNavigationKey = ev.keyCode; + switch( pressedKeyboardNavigationKey ) + { + case KeyCode.UpArrow: + case KeyCode.DownArrow: + case KeyCode.LeftArrow: + case KeyCode.RightArrow: + case KeyCode.PageUp: + case KeyCode.PageDown: + case KeyCode.Home: + case KeyCode.End: + case KeyCode.F: treeViewKeyboardNavigation = true; break; + } + + SearchResultTooltip.Hide(); + } + else if( ( ev.type == EventType.ValidateCommand || ev.type == EventType.ExecuteCommand ) && ev.commandName == "Find" && treeView.HasFocus() ) + { + if( ev.type == EventType.ExecuteCommand ) + { + treeViewSearchField.SetFocus(); + + // Framed rect padding: Top = 2, Bottom = 2 + the first element in the TreeView + scrollPosition = FrameRectInScrollView( scrollPosition, new Vector2( searchFieldRect.y - 2f, searchFieldRect.yMax + EditorGUIUtility.singleLineHeight + 2f ), window.position.height ); + window.Repaint(); + + SearchResultTooltip.Hide(); + } + + ev.Use(); + } + else if( ev.type == EventType.ScrollWheel ) + SearchResultTooltip.Hide(); + + bool isFirstRowSelected = false, isLastRowSelected = false, isSelectedRowExpanded = false, canExpandSelectedRow = false; + if( treeViewKeyboardNavigation && treeView.HasFocus() && treeView.HasSelection() ) + treeView.GetRowStateWithId( treeViewState.lastClickedID, out isFirstRowSelected, out isLastRowSelected, out isSelectedRowExpanded, out canExpandSelectedRow ); + + Rect treeViewRect = EditorGUILayout.GetControlRect( false, treeView.totalHeight ); + if( ev.type == EventType.Repaint ) + { + lastTreeViewRect = treeViewRect; + +#if !UNITY_2018_2_OR_NEWER + // TreeView calls RowGUI for all rows instead of only the visible rows on early Unity versions which leads to performance issues. Do manual row culling on those versions + // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/a048de916b23331bf6dfe92c4a6c205989b83b4f/Editor/Mono/GUI/TreeView/TreeViewGUI.cs#L273-L276 + float topPixel = scrollPosition - treeViewRect.y; + float heightInPixels = window.position.height; + treeView.visibleRowTop = (int) Mathf.Floor( topPixel / treeView.rowHeight ); + treeView.visibleRowBottom = treeView.visibleRowTop + (int) Mathf.Ceil( heightInPixels / treeView.rowHeight ); +#endif + } + + treeView.OnGUI( treeViewRect ); + + if( treeViewKeyboardNavigation && treeView.HasFocus() && treeView.HasSelection() ) + { + Rect targetTreeViewRowRect; + if( treeView.GetRowRectWithId( treeViewState.lastClickedID, out targetTreeViewRowRect ) ) + { + // Allow keyboard navigation between different SearchResultGroups' TreeViews + Rect targetTreeViewRect = lastTreeViewRect; + if( !ev.control && !ev.command && !ev.shift ) + { + if( isFirstRowSelected && ( pressedKeyboardNavigationKey == KeyCode.UpArrow || pressedKeyboardNavigationKey == KeyCode.PageUp || pressedKeyboardNavigationKey == KeyCode.Home || ( pressedKeyboardNavigationKey == KeyCode.LeftArrow && !isSelectedRowExpanded ) ) ) + { + int searchResultGroupIndex = searchResult.IndexOf( this ); + for( int i = searchResultGroupIndex - 1; i >= 0; i-- ) + { + if( !searchResult[i].PendingSearch && searchResult[i].IsExpanded && searchResult[i].references.Count > 0 ) + { + searchResult[i].treeView.SetFocus(); + + targetTreeViewRect = searchResult[i].lastTreeViewRect; + targetTreeViewRowRect = searchResult[i].treeView.SelectLastRowAndReturnRect(); + + break; + } + } + } + else if( isLastRowSelected && ( pressedKeyboardNavigationKey == KeyCode.DownArrow || pressedKeyboardNavigationKey == KeyCode.PageDown || pressedKeyboardNavigationKey == KeyCode.End || ( pressedKeyboardNavigationKey == KeyCode.RightArrow && !canExpandSelectedRow ) ) ) + { + int searchResultGroupIndex = searchResult.IndexOf( this ); + for( int i = searchResultGroupIndex + 1; i < searchResult.NumberOfGroups; i++ ) + { + if( !searchResult[i].PendingSearch && searchResult[i].IsExpanded && searchResult[i].references.Count > 0 ) + { + searchResult[i].treeView.SetFocus(); + + targetTreeViewRect = searchResult[i].lastTreeViewRect; + targetTreeViewRowRect = searchResult[i].treeView.SelectFirstRowAndReturnRect(); + + break; + } + } + } + } + + // When key event isn't automatically used by the focused TreeView (happens when its search results are empty), if we navigate to + // a new TreeView, key event will be consumed by that TreeView and hence, keyboard navigation will occur twice + if( ev.type != EventType.Used ) + ev.Use(); + + float scrollTop = targetTreeViewRect.y + targetTreeViewRowRect.y; + float scrollBottom = targetTreeViewRect.y + targetTreeViewRowRect.yMax; + + scrollPosition = FrameRectInScrollView( scrollPosition, new Vector2( scrollTop, scrollBottom ), window.position.height ); + window.Repaint(); + } + } + } + } + + return scrollPosition; + } + + // Frame selection (it isn't handled automatically when using an external scroll view) + // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/d0fe81a19ce788fd1d94f826cf797aafc37db8ea/Editor/Mono/GUI/TreeView/TreeViewController.cs#L1329-L1351 + private float FrameRectInScrollView( float scrollPosition, Vector2 rectBounds, float windowHeight ) + { + return Mathf.Clamp( scrollPosition, rectBounds.y - windowHeight, rectBounds.x ); + } + + // Serialize this result group + internal SearchResult.SerializableResultGroup Serialize( Dictionary nodeToIndex, List serializedNodes ) + { + SearchResult.SerializableResultGroup serializedResultGroup = new SearchResult.SerializableResultGroup() + { + title = Title, + type = Type, + isExpanded = IsExpanded, + pendingSearch = PendingSearch, + treeViewState = treeViewState + }; + + if( references != null ) + { + serializedResultGroup.initialSerializedNodes = new List( references.Count ); + + for( int i = 0; i < references.Count; i++ ) + serializedResultGroup.initialSerializedNodes.Add( references[i].SerializeRecursively( nodeToIndex, serializedNodes ) ); + } + + return serializedResultGroup; + } + + // Deserialize this result group from the serialized data + internal void Deserialize( SearchResult.SerializableResultGroup serializedResultGroup, List allNodes ) + { + treeViewState = serializedResultGroup.treeViewState; + + if( serializedResultGroup.initialSerializedNodes != null ) + { + for( int i = 0; i < serializedResultGroup.initialSerializedNodes.Count; i++ ) + references.Add( allNodes[serializedResultGroup.initialSerializedNodes[i]] ); + } + } + + IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) references ).GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) references ).GetEnumerator(); } + } + + // Custom class to hold an object in the path to a reference as a node + public class ReferenceNode + { + internal enum UsedState { Unused, MixedCollapsed, MixedExpanded, Used }; + + public class Link + { + public readonly ReferenceNode targetNode; + public readonly List descriptions; + public bool isWeakLink; // Weak links can be omitted from search results if this ReferenceNode isn't referenced by any other node + + public Link( ReferenceNode targetNode, string description, bool isWeakLink ) + { + this.targetNode = targetNode; + this.descriptions = string.IsNullOrEmpty( description ) ? new List() : new List( 1 ) { description }; + this.isWeakLink = isWeakLink; + } + + public Link( ReferenceNode targetNode, List descriptions, bool isWeakLink ) + { + this.targetNode = targetNode; + this.descriptions = descriptions; + this.isWeakLink = isWeakLink; + } + } + + // Unique identifier is used while serializing the node + private static int uid_last = 0; + private readonly int uid; + + public string Label { get; private set; } + public bool IsMainReference { get; private set; } // True: if belongs to a scene search result group, then it's an object in that scene. If belongs to the assets search result group, then it's an asset + + internal object nodeObject; + private int? instanceId; // instanceId of the nodeObject if it is a Unity object, null otherwise + public Object UnityObject { get { return instanceId.HasValue ? EditorUtility.InstanceIDToObject( instanceId.Value ) : null; } } + + private readonly List links = new List( 2 ); + public int NumberOfOutgoingLinks { get { return links.Count; } } + public Link this[int index] { get { return links[index]; } } + + internal UsedState usedState; + + public ReferenceNode() + { + uid = uid_last++; + usedState = UsedState.Used; + } + + // Add a one-way connection to another node + public void AddLinkTo( ReferenceNode nextNode, string description = null, bool isWeakLink = false ) + { + if( nextNode != null && nextNode != this ) + { + if( !string.IsNullOrEmpty( description ) ) + description = "[" + description + "]"; + + // Avoid duplicate links + for( int i = 0; i < links.Count; i++ ) + { + if( links[i].targetNode == nextNode ) + { + if( !string.IsNullOrEmpty( description ) && !links[i].descriptions.Contains( description ) ) + links[i].descriptions.Add( description ); + + links[i].isWeakLink &= isWeakLink; + return; + } + } + + links.Add( new Link( nextNode, description, isWeakLink ) ); + } + } + + public void RemoveLink( int index ) + { + links.RemoveAt( index ); + } + + public bool RemoveLink( ReferenceNode nextNode ) + { + for( int i = links.Count - 1; i >= 0; i-- ) + { + if( links[i].targetNode == nextNode ) + { + links.RemoveAt( i ); + return true; + } + } + + return false; + } + + public void SortLinks() + { + if( links.Count > 1 ) + { + SearchResult.SortedEntry[] sortedEntries = new SearchResult.SortedEntry[links.Count]; + for( int i = links.Count - 1; i >= 0; i-- ) + sortedEntries[i] = new SearchResult.SortedEntry( links[i] ); + + Array.Sort( sortedEntries ); + + for( int i = 0; i < sortedEntries.Length; i++ ) + links[i] = (Link) sortedEntries[i].entry; + } + } + + public void CopyReferencesTo( ReferenceNode other ) + { + other.links.Clear(); + other.links.AddRange( links ); + } + + // Clear this node so that it can be reused later + public void Clear() + { + nodeObject = null; + links.Clear(); + } + + public void InitializeRecursively() + { + if( Label != null ) // Already initialized + return; + + Object unityObject = nodeObject as Object; + if( unityObject != null ) + { + instanceId = unityObject.GetInstanceID(); + Label = unityObject.name + " (" + unityObject.GetType().Name + ")"; + + if( AssetUsageDetectorSettings.ShowRootAssetName && unityObject.IsAsset() && !AssetDatabase.IsMainAsset( unityObject ) ) + { + string mainAssetName = Path.GetFileNameWithoutExtension( AssetDatabase.GetAssetPath( unityObject ) ); + if( unityObject.name != mainAssetName ) + Label += " "; + } + } + else if( nodeObject != null ) + { + instanceId = null; + Label = nodeObject.GetType() + " object"; + } + else + { + instanceId = null; + Label = "<>"; + } + + nodeObject = null; // Don't hold Object reference, allow Unity to GC used memory + + for( int i = 0; i < links.Count; i++ ) + links[i].targetNode.InitializeRecursively(); + } + + public ReferenceNode CreateReverseGraphRecursively( SearchResultGroup searchResultGroup, List reverseGraphRoots, Dictionary reverseGraphNodes, HashSet objectsToSearchSet ) + { + ReferenceNode result; + if( !reverseGraphNodes.TryGetValue( this, out result ) ) + { + reverseGraphNodes[this] = result = new ReferenceNode() { nodeObject = nodeObject }; + + Object obj = nodeObject as Object; + if( obj && objectsToSearchSet.Contains( obj ) ) + reverseGraphRoots.Add( result ); + //else // When 'else' is uncommented, 'Don't search "Find referenced of" themselves for references" option simply does nothing. I am not entirely sure if commenting it out will have any side effects, so fingers crossed? + { + for( int i = 0; i < links.Count; i++ ) + { + ReferenceNode linkedNode = links[i].targetNode.CreateReverseGraphRecursively( searchResultGroup, reverseGraphRoots, reverseGraphNodes, objectsToSearchSet ); + linkedNode.links.Add( new Link( result, links[i].descriptions, links[i].isWeakLink ) ); + } + } + + if( obj ) + { + if( obj is Component ) + obj = ( (Component) obj ).gameObject; + + switch( searchResultGroup.Type ) + { + case SearchResultGroup.GroupType.Assets: result.IsMainReference = obj.IsAsset() && ( obj is GameObject || ( obj.hideFlags & ( HideFlags.HideInInspector | HideFlags.HideInHierarchy ) ) == HideFlags.None ); break; + case SearchResultGroup.GroupType.ProjectSettings: result.IsMainReference = obj.IsAsset() && AssetDatabase.GetAssetPath( obj ).StartsWith( "ProjectSettings/" ); break; + case SearchResultGroup.GroupType.Scene: + case SearchResultGroup.GroupType.DontDestroyOnLoad: + { + if( obj is GameObject ) + { + Scene scene = ( (GameObject) obj ).scene; + if( scene.IsValid() ) + result.IsMainReference = ( searchResultGroup.Type == SearchResultGroup.GroupType.Scene ) ? scene.path == searchResultGroup.ScenePath : scene.name == "DontDestroyOnLoad"; + } + + break; + } + } + } + } + + return result; + } + + public void RemoveRedundantLinksRecursively( HashSet visitedNodes ) + { + if( !visitedNodes.Add( this ) ) + return; + + List stack = null; + for( int i = links.Count - 1; i >= 0; i-- ) + { + if( !links[i].isWeakLink ) + continue; + + if( links[i].targetNode.links.Count == 0 ) + links.RemoveAt( i ); + else + { + if( stack == null ) + stack = new List( 2 ); + else + stack.Clear(); + + if( !links[i].targetNode.CheckForNonWeakLinksRecursively( stack ) ) + links.RemoveAt( i ); + } + } + + for( int i = links.Count - 1; i >= 0; i-- ) + links[i].targetNode.RemoveRedundantLinksRecursively( visitedNodes ); + } + + private bool CheckForNonWeakLinksRecursively( List stack ) + { + if( stack.Contains( this ) || links.Count == 0 ) + return false; + + for( int i = links.Count - 1; i >= 0; i-- ) + { + if( !links[i].isWeakLink ) + return true; + } + + stack.Add( this ); + + for( int i = links.Count - 1; i >= 0; i-- ) + { + if( links[i].targetNode.CheckForNonWeakLinksRecursively( stack ) ) + return true; + } + + stack.RemoveAt( stack.Count - 1 ); + + return false; + } + + // Serialize this node and its connected nodes recursively + internal int SerializeRecursively( Dictionary nodeToIndex, List serializedNodes ) + { + int index; + if( nodeToIndex.TryGetValue( this, out index ) ) + return index; + + SearchResult.SerializableNode serializedNode = new SearchResult.SerializableNode() + { + label = Label, + isMainReference = IsMainReference, + instanceId = instanceId ?? 0, + isUnityObject = instanceId.HasValue, + usedState = usedState + }; + + index = serializedNodes.Count; + nodeToIndex[this] = index; + serializedNodes.Add( serializedNode ); + + if( links.Count > 0 ) + { + serializedNode.links = new List( links.Count ); + serializedNode.linkDescriptions = new List( links.Count ); + serializedNode.linkWeakStates = new List( links.Count ); + + for( int i = 0; i < links.Count; i++ ) + { + serializedNode.links.Add( links[i].targetNode.SerializeRecursively( nodeToIndex, serializedNodes ) ); + serializedNode.linkDescriptions.Add( new SearchResult.SerializableNode.SerializableLinkDescriptions() { value = links[i].descriptions } ); + serializedNode.linkWeakStates.Add( links[i].isWeakLink ); + } + } + + return index; + } + + // Deserialize this node and its links from the serialized data + internal void Deserialize( SearchResult.SerializableNode serializedNode, List allNodes ) + { + if( serializedNode.isUnityObject ) + instanceId = serializedNode.instanceId; + else + instanceId = null; + + Label = serializedNode.label; + IsMainReference = serializedNode.isMainReference; + usedState = serializedNode.usedState; + + if( serializedNode.links != null ) + { + for( int i = 0; i < serializedNode.links.Count; i++ ) + links.Add( new Link( allNodes[serializedNode.links[i]], serializedNode.linkDescriptions[i].value, serializedNode.linkWeakStates[i] ) ); + } + } + + public override int GetHashCode() + { + return uid; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchResult.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/SearchResult.cs.meta new file mode 100644 index 0000000..7764d6b --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca236e4f3c5a9f447be89f0e61e485fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTooltip.cs b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTooltip.cs new file mode 100644 index 0000000..9785717 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTooltip.cs @@ -0,0 +1,91 @@ +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace AssetUsageDetectorNamespace +{ + public class SearchResultTooltip : EditorWindow + { + private static SearchResultTooltip mainWindow; + private static string tooltip; + + private static GUIStyle m_style; + internal static GUIStyle Style + { + get + { + if( m_style == null ) + { + m_style = (GUIStyle) typeof( EditorStyles ).GetProperty( "tooltip", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).GetValue( null, null ); + m_style.richText = true; + } + + return m_style; + } + } + + public static void Show( Rect sourcePosition, string tooltip ) + { + Vector2 preferredSize = Style.CalcSize( new GUIContent( tooltip ) ) + Style.contentOffset + new Vector2( Style.padding.horizontal + Style.margin.horizontal, Style.padding.vertical + Style.margin.vertical ); + Rect preferredPosition; + + Rect positionLeft = new Rect( sourcePosition.position - new Vector2( preferredSize.x, 0f ), preferredSize ); + Rect screenFittedPositionLeft = Utilities.GetScreenFittedRect( positionLeft ); + + Vector2 positionOffset = positionLeft.position - screenFittedPositionLeft.position; + Vector2 sizeOffset = positionLeft.size - screenFittedPositionLeft.size; + if( positionOffset.sqrMagnitude <= 400f && sizeOffset.sqrMagnitude <= 400f ) + preferredPosition = screenFittedPositionLeft; + else + { + Rect positionRight = new Rect( sourcePosition.position + new Vector2( sourcePosition.width, 0f ), preferredSize ); + Rect screenFittedPositionRight = Utilities.GetScreenFittedRect( positionRight ); + + Vector2 positionOffset2 = positionRight.position - screenFittedPositionRight.position; + Vector2 sizeOffset2 = positionRight.size - screenFittedPositionRight.size; + if( positionOffset2.magnitude + sizeOffset2.magnitude < positionOffset.magnitude + sizeOffset.magnitude ) + preferredPosition = screenFittedPositionRight; + else + preferredPosition = screenFittedPositionLeft; + } + + // Don't lose focus to the previous window + EditorWindow prevFocusedWindow = focusedWindow; + + if( !mainWindow ) + { + mainWindow = CreateInstance(); + mainWindow.ShowPopup(); + } + + SearchResultTooltip.tooltip = tooltip; + mainWindow.minSize = preferredPosition.size; + mainWindow.position = preferredPosition; + mainWindow.Repaint(); + + if( prevFocusedWindow ) + prevFocusedWindow.Focus(); + } + + public static void Hide() + { + if( mainWindow ) + { + mainWindow.Close(); + mainWindow = null; + } + } + + private void OnGUI() + { + // If somehow the tooltip isn't automatically closed, allow closing it by clicking on it + if( Event.current.type == EventType.MouseDown ) + { + Hide(); + GUIUtility.ExitGUI(); + } + + GUI.Label( new Rect( Vector2.zero, position.size ), tooltip, Style ); + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTooltip.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTooltip.cs.meta new file mode 100644 index 0000000..06a2662 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTooltip.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: c8bd4351b5024324ca5974ebcad1dde3 +timeCreated: 1639247551 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTreeView.cs b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTreeView.cs new file mode 100644 index 0000000..cbb68fa --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTreeView.cs @@ -0,0 +1,1353 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text; +using UnityEditor; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +#if UNITY_2021_2_OR_NEWER +using PrefabStage = UnityEditor.SceneManagement.PrefabStage; +using PrefabStageUtility = UnityEditor.SceneManagement.PrefabStageUtility; +#elif UNITY_2018_3_OR_NEWER +using PrefabStage = UnityEditor.Experimental.SceneManagement.PrefabStage; +using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility; +#endif + +namespace AssetUsageDetectorNamespace +{ + [System.Serializable] + public class SearchResultTreeViewState : TreeViewState + { + // - initialNodeId is serialized because we want to preserve the expanded states of the TreeViewItems after domain reload and + // it's only possible if TreeView is reconstructed with the same ids + // - finalNodeId is serialized because if the same id used for multiple TreeViewItems across multiple TreeViews, strange issues occur. + // Thus, each new TreeView will set its initialNodeId to the previous TreeView's finalNodeId + // - Each TreeViewItem's id is different even if two TreeViewItems point to the exact same ReferenceNode. That's because TreeView + // doesn't work well when some TreeViewItems share the same id (e.g. while navigating the tree with arrow keys) + public int initialNodeId, finalNodeId; + + // Not using the built-in searchString and hasSearch properties of TreeView because: + // - This search algorithm is a bit more complicated than usual, we don't flatten the tree during the search + // - If code is recompiled while searchString wasn't empty, the tree isn't rebuilt and remains empty (at least on Unity 5.6) + public string searchTerm; + public SearchResultTreeView.SearchMode searchMode = SearchResultTreeView.SearchMode.All; + public bool selectionChangedDuringSearch; + + public List preSearchExpandedIds; + } + + public class SearchResultTreeView : TreeView + { + public enum TreeType { Normal, UnusedObjects, IsolatedView }; + public enum SearchMode { SearchedObjectsOnly, ReferencesOnly, All }; + + private class ReferenceNodeData + { + public readonly TreeViewItem item; + public readonly ReferenceNode node; + public readonly ReferenceNodeData parent; + public readonly int linkIndex; + public bool isLastLink; + public bool isDuplicate; + public bool shouldExpandAfterSearch; + + private string m_tooltipText; + public string tooltipText + { + get + { + if( m_tooltipText != null ) + return m_tooltipText; + + return GetTooltipText( Utilities.stringBuilder ); + } + } + + public ReferenceNodeData( TreeViewItem item, ReferenceNode node, ReferenceNodeData parent, int linkIndex ) + { + this.item = item; + this.node = node; + this.parent = parent; + this.linkIndex = linkIndex; + } + + private string GetTooltipText( StringBuilder sb ) + { + sb.Length = 0; + sb.Append( "- " ).Append( node.Label ); + + if( parent != null ) + { + sb.Append( "\n" ); + + if( parent.node[linkIndex].descriptions.Count > 0 ) + { + List linkDescriptions = parent.node[linkIndex].descriptions; + for( int i = 0; i < linkDescriptions.Count; i++ ) + sb.Append( " " ).Append( linkDescriptions[i] ).Append( "\n" ); + } + + if( parent.m_tooltipText != null ) + sb.Append( parent.m_tooltipText ); + else // Cache parents' tooltips along the way because they'll likely be reused frequently. We need to use new StringBuilder instances for them + sb.Append( parent.GetTooltipText( new StringBuilder( 256 ) ) ); + } + + m_tooltipText = sb.ToString(); + return m_tooltipText; + } + + public void ResetTooltip() + { + m_tooltipText = null; + } + } + + private const float SEARCHED_OBJECTS_BORDER_THICKNESS = 1f; +#if UNITY_2019_3_OR_NEWER + private const float TREE_VIEW_LINES_THICKNESS = 1.5f; // There are inexplicable spaces between the vertical and horizontal lines if we don't change thickness by 0.5f on 2019.3+ +#else + private const float TREE_VIEW_LINES_THICKNESS = 2f; +#endif + private const float HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS = TREE_VIEW_LINES_THICKNESS * 2f; + + private readonly new SearchResultTreeViewState state; + + private readonly List references; + private readonly List idToNodeDataLookup = new List( 128 ); + + private readonly HashSet selectedReferenceNodes = new HashSet(); + private readonly HashSet selectedReferenceNodesHierarchyIds = new HashSet(); + private readonly HashSet selectedReferenceNodesHierarchyIndirectIds = new HashSet(); + + private readonly HashSet usedObjectsSet; + + private readonly TreeType treeType; + private readonly bool hideDuplicateRows; + private readonly bool hideReduntantPrefabVariantLinks; + + private bool isSearching; + +#if !UNITY_2018_2_OR_NEWER + public int visibleRowTop = 0, visibleRowBottom = int.MaxValue; +#endif + + private readonly CompareInfo textComparer = new CultureInfo( "en-US" ).CompareInfo; + private readonly CompareOptions textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace; + + private readonly GUIContent sharedGUIContent = new GUIContent(); + private GUIStyle foldoutLabelStyle; + private Texture2D whiteGradientTexture; + private string highlightedSearchTextColor; + + private ReferenceNodeData prevHoveredData, hoveredData; + private Rect hoveredDataRect; + + private bool isTreeViewEmpty; + private bool isLMBDown; + + private double customTooltipShowTime; + + public new float rowHeight + { + get { return base.rowHeight; } + set + { + base.rowHeight = value; +#if !UNITY_2019_3_OR_NEWER + customFoldoutYOffset = ( value - EditorGUIUtility.singleLineHeight ) * 0.5f; +#endif + } + } + + // Avoid using these properties of TreeView by mistake + [System.Obsolete] private new string searchString { get; } + [System.Obsolete] private new bool hasSearch { get; } + + public SearchResultTreeView( SearchResultTreeViewState state, List references, TreeType treeType, HashSet usedObjectsSet, bool hideDuplicateRows, bool hideReduntantPrefabVariantLinks, bool usesExternalScrollView ) : base( state ) + { + this.state = state; + this.references = references; + this.treeType = treeType; + this.hideDuplicateRows = hideDuplicateRows; + this.hideReduntantPrefabVariantLinks = hideReduntantPrefabVariantLinks; + + highlightedSearchTextColor = ""; + + rowHeight = EditorGUIUtility.singleLineHeight + AssetUsageDetectorSettings.ExtraRowHeight; + + if( treeType == TreeType.UnusedObjects ) + { + showBorder = true; + this.usedObjectsSet = usedObjectsSet; + } + + if( treeType != TreeType.IsolatedView ) + { + // Draw only the visible rows. This requires setting useScrollView to false because we are using an external scroll view: https://docs.unity3d.com/ScriptReference/IMGUI.Controls.TreeView-useScrollView.html +#if UNITY_2018_2_OR_NEWER + useScrollView = false; +#else + // In my tests, SetUseScrollView seems to have no effect unfortunately but let's keep this line in case it fixes some other issues with the external scroll view + object treeViewController = typeof( TreeView ).GetField( "m_TreeView", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( this ); + treeViewController.GetType().GetMethod( "SetUseScrollView", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( treeViewController, new object[1] { false } ); +#endif + } + + isSearching = !string.IsNullOrEmpty( state.searchTerm ); + + Reload(); + + if( HasSelection() ) + RefreshSelectedNodes( GetSelection() ); + } + + public void RefreshSearch( string prevSearchTerm ) + { + bool wasSearchTermEmpty = string.IsNullOrEmpty( prevSearchTerm ); + bool isSearchTermEmpty = string.IsNullOrEmpty( state.searchTerm ); + + isSearching = !isSearchTermEmpty; + + if( !wasSearchTermEmpty || !isSearchTermEmpty ) + { + Reload(); + + if( !isSearchTermEmpty ) + { + if( wasSearchTermEmpty ) + { + state.preSearchExpandedIds = new List( GetExpanded() ?? new int[0] ); + state.selectionChangedDuringSearch = false; + } + + ExpandMatchingSearchResults(); + } + else if( !wasSearchTermEmpty && state.preSearchExpandedIds != null && state.preSearchExpandedIds.Count > 0 ) + { + List expandedIds = state.preSearchExpandedIds; + HashSet expandedIdsSet = new HashSet( expandedIds ); + if( state.selectionChangedDuringSearch ) + { + IList selection = GetSelection(); + for( int i = 0; i < selection.Count; i++ ) + { + for( TreeViewItem item = GetDataFromId( selection[i] ).item; item != null; item = item.parent ) + { + if( expandedIdsSet.Add( item.id ) ) + expandedIds.Add( item.id ); + else + break; + } + } + } + + SetExpanded( state.preSearchExpandedIds ); + expandedIds.Clear(); + } + + if( HasSelection() ) + RefreshSelectedNodes( GetSelection() ); + } + } + + protected override TreeViewItem BuildRoot() + { + TreeViewItem root = new TreeViewItem { id = state.initialNodeId, depth = -1, displayName = "Root" }; + int id = state.initialNodeId + 1; + + idToNodeDataLookup.Clear(); + + List stack = new List( 8 ); + HashSet processedNodes = null; + if( hideDuplicateRows ) + { + processedNodes = new HashSet(); + for( int i = references.Count - 1; i >= 0; i-- ) + { + // Don't mark root nodes as duplicates unless we're in ReferencesOnly search mode (in which case, it's just technically unfeasible to know which root nodes will be displayed in advance) + if( !isSearching || ( state.searchMode != SearchMode.ReferencesOnly && textComparer.IndexOf( references[i].Label, state.searchTerm, textCompareOptions ) >= 0 ) ) + processedNodes.Add( references[i] ); + } + } + + for( int i = 0; i < references.Count; i++ ) + GenerateRowsRecursive( root, references[i], null, i, 0, null, stack, processedNodes, ref id ); + + isTreeViewEmpty = !root.hasChildren; + if( isTreeViewEmpty ) // May happen if all items are hidden inside HideItems function or there are no matching search results. If we don't create a dummy child, Unity throws an exception + root.AddChild( new TreeViewItem( state.initialNodeId + 1 ) ); // If we don't give it a valid id, some functions throw exceptions when there are no matching search results + else + GetDataFromId( root.children[root.children.Count - 1].id ).isLastLink = true; + + state.finalNodeId = id + 1; + + return root; + } + + private bool GenerateRowsRecursive( TreeViewItem parent, ReferenceNode referenceNode, ReferenceNodeData parentData, int siblingIndex, int depth, bool? itemForcedVisibility, List stack, HashSet processedNodes, ref int id ) + { + TreeViewItem item = new TreeViewItem( id++, depth, "" ); + ReferenceNodeData data = new ReferenceNodeData( item, referenceNode, parentData, siblingIndex ); + + bool shouldShowItem; + if( itemForcedVisibility.HasValue ) + shouldShowItem = itemForcedVisibility.Value; + else + { + if( !isSearching ) + shouldShowItem = true; + else if( state.searchMode == SearchMode.All || ( ( depth == 0 ) == ( state.searchMode == SearchMode.SearchedObjectsOnly ) ) ) + { + shouldShowItem = textComparer.IndexOf( referenceNode.Label, state.searchTerm, textCompareOptions ) >= 0; + if( !shouldShowItem && depth > 0 ) + { + List descriptions = parentData.node[siblingIndex].descriptions; + for( int i = descriptions.Count - 1; i >= 0; i-- ) + { + if( textComparer.IndexOf( descriptions[i], state.searchTerm, textCompareOptions ) >= 0 ) + { + shouldShowItem = true; + break; + } + } + } + + data.shouldExpandAfterSearch = shouldShowItem; + + if( state.searchMode == SearchMode.SearchedObjectsOnly || ( state.searchMode == SearchMode.All && shouldShowItem ) ) + itemForcedVisibility = shouldShowItem; + } + else + shouldShowItem = false; + } + + idToNodeDataLookup.Add( data ); + + // Disallow recursion (stack) because it would crash Unity + if( referenceNode.NumberOfOutgoingLinks > 0 && !stack.ContainsFast( referenceNode ) ) + { + // Add children only if hideDuplicateRows is false (processedNodes == null) or this node hasn't been seen before + if( processedNodes != null && !processedNodes.Add( referenceNode ) && depth > 0 ) // "depth > 0": Root nodes are either added to processedNodes prior to generating rows (so that they're never marked as duplicate), or they just shouldn't be trimmed + data.isDuplicate = true; + else + { + stack.Add( referenceNode ); + + // Generate child items even if they will be forced invisible so that each visible row's id is deterministic and doesn't change when some rows become invisible + for( int i = 0; i < referenceNode.NumberOfOutgoingLinks; i++ ) + shouldShowItem |= GenerateRowsRecursive( item, referenceNode[i].targetNode, data, i, depth + 1, itemForcedVisibility, stack, processedNodes, ref id ); + + stack.RemoveAt( stack.Count - 1 ); + } + } + + if( shouldShowItem ) + { + if( item.hasChildren ) + GetDataFromId( item.children[item.children.Count - 1].id ).isLastLink = true; + + parent.AddChild( item ); + return true; + } + + return false; + } + + private ReferenceNodeData GetDataFromId( int id ) + { + return idToNodeDataLookup[id - state.initialNodeId - 1]; + } + + public override void OnGUI( Rect rect ) + { + // Disallow clicking on "No matching results" text when in search mode + bool guiEnabled = GUI.enabled; + if( isTreeViewEmpty ) + GUI.enabled = false; + + // Mouse and special keyboard events are already in Used state in CommandEventHandling, so we need to process them here + Event ev = Event.current; + if( ev.type == EventType.MouseDown ) + { + if( ev.button == 0 ) + isLMBDown = true; + } + else if( ev.type == EventType.MouseUp ) + { + if( ev.button == 0 ) + isLMBDown = false; + } + else if( ev.type == EventType.MouseMove ) + hoveredData = null; + else if( ev.type == EventType.KeyDown ) + { + if( ( ev.keyCode == KeyCode.Return || ev.keyCode == KeyCode.KeypadEnter ) && HasSelection() && HasFocus() ) + { + DoubleClickedItem( state.lastClickedID ); + ev.Use(); + } + } + + base.OnGUI( rect ); + + if( prevHoveredData != hoveredData ) + { + if( AssetUsageDetectorSettings.CustomTooltipDelay > 0f ) + EditorApplication.update -= ShowTooltipDelayed; + + prevHoveredData = hoveredData; + if( hoveredData != null ) + { + if( AssetUsageDetectorSettings.CustomTooltipDelay <= 0f ) + SearchResultTooltip.Show( hoveredDataRect, hoveredData.tooltipText ); + else + { + customTooltipShowTime = EditorApplication.timeSinceStartup + AssetUsageDetectorSettings.CustomTooltipDelay; + EditorApplication.update += ShowTooltipDelayed; + } + } + else + SearchResultTooltip.Hide(); + + Repaint(); + } + + GUI.enabled = guiEnabled; + } + + protected override void RowGUI( RowGUIArgs args ) + { +#if !UNITY_2018_2_OR_NEWER + // Do manual row culling on early Unity versions + if( args.row < visibleRowTop || args.row > visibleRowBottom ) + return; +#endif + + if( isTreeViewEmpty ) + { + EditorGUI.LabelField( args.rowRect, "No matching results..." ); + return; + } + + Event ev = Event.current; + ReferenceNodeData data = GetDataFromId( args.item.id ); + Rect rect = args.rowRect; + + if( string.IsNullOrEmpty( args.item.displayName ) ) + { + Object unityObject = data.node.UnityObject; + if( unityObject ) + args.item.icon = AssetPreview.GetMiniThumbnail( unityObject ); + + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + + if( data.isDuplicate ) + sb.Append( "[D] " ); + + if( data.parent == null ) + { + if( treeType != TreeType.UnusedObjects ) + sb.Append( "" ); + else if( data.node.usedState == ReferenceNode.UsedState.MixedCollapsed ) + sb.Append( "[!] " ); + else if( data.node.usedState == ReferenceNode.UsedState.MixedExpanded ) + sb.Append( "[!] " ); + + if( !isSearching || state.searchMode == SearchMode.ReferencesOnly ) + sb.Append( data.node.Label ); + else + HighlightSearchTermInString( sb, data.node.Label ); + + if( treeType != TreeType.UnusedObjects ) + sb.Append( "" ); + } + else + { + List linkDescriptions = data.parent.node[data.linkIndex].descriptions; + if( linkDescriptions.Count > 0 ) + { + if( !isSearching || state.searchMode == SearchMode.SearchedObjectsOnly ) + sb.Append( data.node.Label ).Append( " " ).Append( linkDescriptions[0] ); + else + { + HighlightSearchTermInString( sb, data.node.Label ); + sb.Append( " " ); + HighlightSearchTermInString( sb, linkDescriptions[0] ); + } + + if( linkDescriptions.Count > 1 ) + { + bool shouldHighlightRemainingLinkDescriptions = false; + if( isSearching && state.searchMode != SearchMode.SearchedObjectsOnly ) + { + for( int i = linkDescriptions.Count - 1; i > 0; i-- ) + { + if( textComparer.IndexOf( linkDescriptions[i], state.searchTerm, textCompareOptions ) >= 0 ) + { + shouldHighlightRemainingLinkDescriptions = true; + sb.Append( highlightedSearchTextColor ); + + break; + } + } + } + + sb.Append( " and " ).Append( linkDescriptions.Count - 1 ).Append( " more" ); + + if( shouldHighlightRemainingLinkDescriptions ) + sb.Append( "" ); + } + + sb.Append( "" ); + } + else if( isSearching && state.searchMode != SearchMode.SearchedObjectsOnly ) + HighlightSearchTermInString( sb, data.node.Label ); + else + sb.Append( data.node.Label ); + } + + args.item.displayName = sb.ToString(); + } + + sharedGUIContent.text = args.item.displayName; + sharedGUIContent.tooltip = AssetUsageDetectorSettings.ShowUnityTooltip ? data.tooltipText : null; + sharedGUIContent.image = args.item.icon; + + if( ev.type == EventType.Repaint ) + { + if( treeType != TreeType.UnusedObjects ) + { + if( args.item.depth == 0 ) + { + Color guiColor = GUI.color; + + // Draw background + if( !args.selected ) + { + GUI.color = guiColor * ( ( AssetUsageDetectorSettings.ApplySelectedRowParentsTintToRootRows && selectedReferenceNodesHierarchyIds.Contains( args.item.id ) ) ? AssetUsageDetectorSettings.SelectedRowParentsTint : AssetUsageDetectorSettings.RootRowsBackgroundColor ); + GUI.DrawTexture( rect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + } + + // Draw border: https://github.com/Unity-Technologies/UnityCsReference/blob/33cbfe062d795667c39e16777230e790fcd4b28b/Editor/Mono/GUI/InternalEditorGUI.cs#L262-L275 + if( AssetUsageDetectorSettings.RootRowsBorderColor.a > 0f ) + { + GUI.color = guiColor * AssetUsageDetectorSettings.RootRowsBorderColor; + GUI.DrawTexture( new Rect( rect.x, rect.y, rect.width, SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + + // Draw bottom border only if there isn't another searched object immediately below this one (otherwise, this bottom border and the following top border are drawn at the same space, resulting in darker shade for that edge) + if( data.isLastLink || ( args.item.hasChildren && IsExpanded( args.item.id ) ) ) + { + GUI.DrawTexture( new Rect( rect.x, rect.yMax - SEARCHED_OBJECTS_BORDER_THICKNESS, rect.width, SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + GUI.DrawTexture( new Rect( rect.x, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height - 2f * SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + GUI.DrawTexture( new Rect( rect.xMax - SEARCHED_OBJECTS_BORDER_THICKNESS, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height - 2f * SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + } + else + { + GUI.DrawTexture( new Rect( rect.x, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + GUI.DrawTexture( new Rect( rect.xMax - SEARCHED_OBJECTS_BORDER_THICKNESS, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + } + } + + GUI.color = guiColor; + } + else + { + if( !args.selected ) + { + if( selectedReferenceNodesHierarchyIds.Contains( args.item.id ) ) + EditorGUI.DrawRect( rect, AssetUsageDetectorSettings.SelectedRowParentsTint ); + + if( data.node.IsMainReference ) + EditorGUI.DrawRect( new Rect( rect.x, rect.y, GetContentIndent( args.item ) - 1f, rect.height ), AssetUsageDetectorSettings.MainReferencesBackgroundColor ); + } + } + } + else + { + if( !args.selected && data.node.usedState == ReferenceNode.UsedState.Used ) + EditorGUI.DrawRect( new Rect( rect.x, rect.y, GetContentIndent( args.item ) - 1f, rect.height ), AssetUsageDetectorSettings.MainReferencesBackgroundColor ); + } + + if( !isLMBDown && treeType != TreeType.UnusedObjects && !args.selected && selectedReferenceNodes.Contains( data.node ) ) + { + if( !whiteGradientTexture ) + { + whiteGradientTexture = new Texture2D( 2, 1, TextureFormat.RGBA32, false ) + { + hideFlags = HideFlags.HideAndDontSave, + alphaIsTransparency = true, + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp + }; + + whiteGradientTexture.SetPixels32( new Color32[2] { Color.white, new Color32( 255, 255, 255, 0 ) } ); + whiteGradientTexture.Apply( false, true ); + } + + Color guiColor = GUI.color; + GUI.color = guiColor * AssetUsageDetectorSettings.SelectedRowOccurrencesColor; + GUI.DrawTexture( new Rect( GetContentIndent( args.item ), rect.y, 125f, rect.height ), whiteGradientTexture, ScaleMode.StretchToFill, true, 0f ); + GUI.color = guiColor; + } + + if( hoveredData == data ) + EditorGUI.DrawRect( rect, new Color( 0.5f, 0.5f, 0.5f, 0.25f ) ); + + if( AssetUsageDetectorSettings.ShowTreeLines && args.item.depth > 0 ) + { + // I was using EditorGUI.DrawRect here but looking at its source code, it's more performant to call GUI.DrawTexture directly: https://github.com/Unity-Technologies/UnityCsReference/blob/e740821767d2290238ea7954457333f06e952bad/Editor/Mono/GUI/InternalEditorGUI.cs#L246-L255 + Color guiColor = GUI.color; + bool shouldHighlightTreeLine; + + Rect verticalLineRect = new Rect( rect.x + GetContentIndent( args.item.parent ) - ( foldoutWidth + TREE_VIEW_LINES_THICKNESS ) * 0.5f - 2f, rect.y, TREE_VIEW_LINES_THICKNESS, rect.height ); + Rect horizontalLineRect = new Rect( verticalLineRect.x, verticalLineRect.y + ( verticalLineRect.height - TREE_VIEW_LINES_THICKNESS ) * 0.5f, foldoutWidth + TREE_VIEW_LINES_THICKNESS - 4f, TREE_VIEW_LINES_THICKNESS ); + + for( ReferenceNodeData parentData = data.parent; parentData.parent != null; parentData = parentData.parent ) + { + if( !parentData.isLastLink ) + { + shouldHighlightTreeLine = selectedReferenceNodesHierarchyIndirectIds.Contains( parentData.item.id ); + Rect _verticalLineRect = new Rect( verticalLineRect.x - depthIndentWidth * ( args.item.depth - parentData.item.depth ), verticalLineRect.y, verticalLineRect.width, verticalLineRect.height ); + if( shouldHighlightTreeLine ) + { + _verticalLineRect.x -= ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; + _verticalLineRect.width = HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS; + } + + GUI.color = guiColor * ( shouldHighlightTreeLine ? AssetUsageDetectorSettings.HighlightedTreeLinesColor : AssetUsageDetectorSettings.TreeLinesColor ); + GUI.DrawTexture( _verticalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + } + } + + bool isInSelectedReferenceNodesHierarchy = selectedReferenceNodesHierarchyIds.Contains( args.item.id ); + if( isInSelectedReferenceNodesHierarchy ) + { + horizontalLineRect.y -= ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; + horizontalLineRect.height = HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS; + } + + GUI.color = guiColor * ( isInSelectedReferenceNodesHierarchy ? AssetUsageDetectorSettings.HighlightedTreeLinesColor : AssetUsageDetectorSettings.TreeLinesColor ); + GUI.DrawTexture( horizontalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + + if( data.isLastLink ) + verticalLineRect.height = ( verticalLineRect.height + TREE_VIEW_LINES_THICKNESS ) * 0.5f; + + GUI.color = guiColor * AssetUsageDetectorSettings.TreeLinesColor; + GUI.DrawTexture( verticalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + + bool isInSelectedReferenceNodesIndirectHierarchy = selectedReferenceNodesHierarchyIndirectIds.Contains( args.item.id ); + if( isInSelectedReferenceNodesHierarchy || isInSelectedReferenceNodesIndirectHierarchy ) + { + GUI.color = guiColor * AssetUsageDetectorSettings.HighlightedTreeLinesColor; + + if( isInSelectedReferenceNodesHierarchy && !isInSelectedReferenceNodesIndirectHierarchy ) + { + if( !data.isLastLink ) + verticalLineRect.height = ( verticalLineRect.height + HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS ) * 0.5f; + else + verticalLineRect.height += ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; + } + + verticalLineRect.x -= ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; + verticalLineRect.width = HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS; + + GUI.DrawTexture( verticalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); + } + + GUI.color = guiColor; + } + + rect.xMin += GetContentIndent( args.item ); + rect.y += AssetUsageDetectorSettings.ExtraRowHeight * 0.5f; +#if !UNITY_2019_3_OR_NEWER + rect.y -= 2f; +#endif + rect.height += 4f; // Incrementing height fixes cropped icon issue on Unity 2019.2 or earlier + + if( foldoutLabelStyle == null ) + foldoutLabelStyle = new GUIStyle( DefaultStyles.foldoutLabel ) { richText = true }; + + foldoutLabelStyle.Draw( rect, sharedGUIContent, false, false, args.selected && args.focused, args.selected ); + + // The only way to support Unity's tooltips seems to be by drawing an invisible GUI.Label over our own label + if( sharedGUIContent.tooltip != null ) + { + sharedGUIContent.text = ""; + sharedGUIContent.image = null; + GUI.Label( rect, sharedGUIContent, foldoutLabelStyle ); + } + } + else if( ev.type == EventType.MouseDown ) + { + if( ev.button == 2 && rect.Contains( ev.mousePosition ) ) + { + HideItems( new int[1] { args.item.id } ); + GUIUtility.ExitGUI(); + } + } + else if( ev.type == EventType.MouseMove ) + { + if( hoveredData != data && AssetUsageDetectorSettings.ShowCustomTooltip && rect.Contains( ev.mousePosition ) ) + { + hoveredData = data; + hoveredDataRect = new Rect( GUIUtility.GUIToScreenPoint( rect.position ), new Vector2( EditorGUIUtility.currentViewWidth, 0f ) ); + } + } + } + + protected override void SelectionChanged( IList selectedIds ) + { + if( isTreeViewEmpty ) + return; + + RefreshSelectedNodes( selectedIds ); + + if( selectedIds.Count == 0 ) + return; + + if( isSearching ) + state.selectionChangedDuringSearch = true; + + Object selection, pingTarget = null; + List selectedUnityObjects = new List( selectedIds.Count ); + for( int i = 0; i < selectedIds.Count; i++ ) + { + Object obj = GetDataFromId( selectedIds[i] ).node.UnityObject; + if( obj ) + { + obj.GetObjectsToSelectAndPing( out selection, out pingTarget ); + if( selection && !selectedUnityObjects.Contains( selection ) ) + selectedUnityObjects.Add( selection ); + } + } + + if( selectedUnityObjects.Count > 0 ) + { + if( AssetUsageDetectorSettings.PingClickedObjects && pingTarget ) + EditorGUIUtility.PingObject( pingTarget ); + if( AssetUsageDetectorSettings.SelectClickedObjects || ( AssetUsageDetectorSettings.SelectDoubleClickedObjects && selectedUnityObjects.Count > 1 ) ) + Selection.objects = selectedUnityObjects.ToArray(); + } + } + + protected override void DoubleClickedItem( int id ) + { + if( isTreeViewEmpty ) + return; + + isLMBDown = false; + + Object clickedObject = GetDataFromId( id ).node.UnityObject; +#if UNITY_2018_3_OR_NEWER + if( clickedObject && clickedObject.IsAsset() ) + { + GameObject clickedPrefabRoot = null; + if( clickedObject is Component ) + clickedPrefabRoot = ( (Component) clickedObject ).transform.root.gameObject; + else if( clickedObject is GameObject ) + clickedPrefabRoot = ( (GameObject) clickedObject ).transform.root.gameObject; + + if( clickedPrefabRoot ) + { + PrefabAssetType prefabAssetType = PrefabUtility.GetPrefabAssetType( clickedPrefabRoot ); + if( prefabAssetType == PrefabAssetType.Regular || prefabAssetType == PrefabAssetType.Variant ) + { + // Try to open the prefab stage of this prefab + string assetPath = AssetDatabase.GetAssetPath( clickedPrefabRoot ); + PrefabStage openPrefabStage = PrefabStageUtility.GetCurrentPrefabStage(); +#if UNITY_2020_1_OR_NEWER + if( openPrefabStage == null || !openPrefabStage.stageHandle.IsValid() || assetPath != openPrefabStage.assetPath ) +#else + if( openPrefabStage == null || !openPrefabStage.stageHandle.IsValid() || assetPath != openPrefabStage.prefabAssetPath ) +#endif + AssetDatabase.OpenAsset( clickedPrefabRoot ); + } + } + } +#endif + + // Ping the clicked GameObject in the open prefab stage + Object selection, pingTarget; + clickedObject.GetObjectsToSelectAndPing( out selection, out pingTarget ); + + if( AssetUsageDetectorSettings.PingClickedObjects && pingTarget ) + EditorGUIUtility.PingObject( pingTarget ); + if( AssetUsageDetectorSettings.SelectDoubleClickedObjects ) + Selection.activeObject = selection; + } + + protected override void ContextClickedItem( int id ) + { + ContextClicked(); + } + + protected override void ContextClicked() + { + if( !isTreeViewEmpty && HasSelection() && HasFocus() ) + { + IList selection = SortItemIDsInRowOrder( GetSelection() ); + + bool hasAnyDuplicateRows = false, hasAnyRowWithOutgoingLinks = false, hasAnyUnusedMixedCollapsedNode = false; + for( int i = 0; i < selection.Count; i++ ) + { + ReferenceNodeData data = GetDataFromId( selection[i] ); + if( !hasAnyDuplicateRows && data.isDuplicate ) + hasAnyDuplicateRows = true; + if( !hasAnyRowWithOutgoingLinks && data.node.NumberOfOutgoingLinks > 0 ) + hasAnyRowWithOutgoingLinks = true; + if( !hasAnyUnusedMixedCollapsedNode && data.node.usedState == ReferenceNode.UsedState.MixedCollapsed ) + hasAnyUnusedMixedCollapsedNode = true; + } + + GenericMenu contextMenu = new GenericMenu(); + + if( treeType != TreeType.IsolatedView ) + contextMenu.AddItem( new GUIContent( "Hide" ), false, () => HideItems( selection ) ); + + if( treeType == TreeType.UnusedObjects ) + { + if( hasAnyUnusedMixedCollapsedNode ) + { + if( contextMenu.GetItemCount() > 0 ) + contextMenu.AddSeparator( "" ); + + contextMenu.AddItem( new GUIContent( "Show Used Children" ), false, ShowChildrenOfSelectedUnusedObjects ); + } + } + else + { + if( contextMenu.GetItemCount() > 0 ) + contextMenu.AddSeparator( "" ); + + if( hasAnyDuplicateRows ) + contextMenu.AddItem( new GUIContent( "Select First Occurrence" ), false, SelectFirstOccurrencesOfDuplicateSelection ); + + contextMenu.AddItem( new GUIContent( "Expand All Occurrences" ), false, ExpandAllSelectionOccurrences ); + } + + if( hasAnyRowWithOutgoingLinks ) + { + if( contextMenu.GetItemCount() > 0 ) + contextMenu.AddSeparator( "" ); + + contextMenu.AddItem( new GUIContent( "Show Children In New Window" ), false, ShowChildrenOfSelectionInNewWindow ); + } + + contextMenu.ShowAsContext(); + + if( Event.current != null && Event.current.type == EventType.ContextClick ) + Event.current.Use(); // It's safer to eat the event and if we don't, the context menu is sometimes displayed with a delay + } + } + + protected override void CommandEventHandling() + { + if( !isTreeViewEmpty && HasFocus() ) // There may be multiple SearchResultTreeViews. Execute the event only for the currently focused one + { + Event ev = Event.current; + if( ev.type == EventType.ValidateCommand || ev.type == EventType.ExecuteCommand ) + { + if( ev.commandName == "Delete" || ev.commandName == "SoftDelete" ) + { + if( ev.type == EventType.ExecuteCommand ) + HideItems( GetSelection() ); + + ev.Use(); + return; + } + } + } + + base.CommandEventHandling(); + } + + protected override bool CanStartDrag( CanStartDragArgs args ) + { + return true; + } + + protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) + { + IList draggedItemIds = args.draggedItemIDs; + if( draggedItemIds.Count == 0 ) + return; + + List draggedUnityObjects = new List( draggedItemIds.Count ); + for( int i = 0; i < draggedItemIds.Count; i++ ) + { + Object obj = GetDataFromId( draggedItemIds[i] ).node.UnityObject; + if( obj ) + draggedUnityObjects.Add( obj ); + } + + if( draggedUnityObjects.Count > 0 ) + { + DragAndDrop.objectReferences = draggedUnityObjects.ToArray(); + DragAndDrop.StartDrag( draggedUnityObjects.Count > 1 ? "" : draggedUnityObjects[0].name ); + } + } + + public void ExpandDirectReferences() + { + List expandedIds = new List( rootItem.children.Count ); + for( int i = 0; i < rootItem.children.Count; i++ ) + expandedIds.Add( rootItem.children[i].id ); + + SetExpanded( expandedIds ); + } + + public void ExpandMainReferences() + { + List expandedIds = new List( references.Count * 12 ); + for( int i = 0; i < rootItem.children.Count; i++ ) + GetMainReferenceIdsRecursive( rootItem.children[i], expandedIds ); + + SetExpanded( expandedIds ); + } + + public void ExpandMatchingSearchResults() + { + if( state.searchMode != SearchMode.ReferencesOnly ) + return; + + List expandedIds = new List( references.Count * 12 ); + for( int i = 0; i < rootItem.children.Count; i++ ) + GetMatchingSearchResultIdsRecursive( rootItem.children[i], expandedIds ); + + SetExpanded( expandedIds ); + } + + private void ExpandAllSelectionOccurrences() + { + IList selection = GetSelection(); + if( selection.Count == 0 ) + return; + + HashSet selectedNodes = new HashSet(); + for( int i = selection.Count - 1; i >= 0; i-- ) + selectedNodes.Add( GetDataFromId( selection[i] ).node ); + + List expandedIds = new List( GetExpanded() ); + for( int i = 0; i < rootItem.children.Count; i++ ) + GetReferenceNodeOccurrenceIdsRecursive( rootItem.children[i], selectedNodes, expandedIds ); + + SetExpanded( expandedIds ); + } + + private bool GetMainReferenceIdsRecursive( TreeViewItem item, List ids ) + { + if( item.depth > 0 && GetDataFromId( item.id ).node.IsMainReference ) + return true; + + bool shouldExpand = false; + if( item.hasChildren ) + { + for( int i = 0; i < item.children.Count; i++ ) + shouldExpand |= GetMainReferenceIdsRecursive( item.children[i], ids ); + } + else + shouldExpand = true; // No main reference is encountered in this branch; expand the whole branch + + if( shouldExpand ) + ids.Add( item.id ); + + return shouldExpand; + } + + private bool GetMatchingSearchResultIdsRecursive( TreeViewItem item, List ids ) + { + bool shouldExpand = false; + if( item.hasChildren ) + { + for( int i = 0; i < item.children.Count; i++ ) + shouldExpand |= GetMatchingSearchResultIdsRecursive( item.children[i], ids ); + } + + if( shouldExpand ) + ids.Add( item.id ); + else + shouldExpand = GetDataFromId( item.id ).shouldExpandAfterSearch; + + return shouldExpand; + } + + private bool GetReferenceNodeOccurrenceIdsRecursive( TreeViewItem item, HashSet referenceNodes, List ids ) + { + bool shouldExpand = false; + if( item.hasChildren ) + { + for( int i = 0; i < item.children.Count; i++ ) + shouldExpand |= GetReferenceNodeOccurrenceIdsRecursive( item.children[i], referenceNodes, ids ); + } + + if( shouldExpand ) + { + if( !ids.Contains( item.id ) ) + ids.Add( item.id ); + + return true; + } + else + return referenceNodes.Contains( GetDataFromId( item.id ).node ); + } + + private void HideItems( IList ids ) + { + if( ids.Count > 0 ) + { + List hiddenNodes = new List( ids.Count ); + List hiddenLinks = new List( ids.Count ); + List newExpandedItemIDs = new List( 32 ); + List newSelectedItemIDs = new List( 16 ); + + for( int i = 0; i < ids.Count; i++ ) + { + ReferenceNodeData data = GetDataFromId( ids[i] ); + if( data.item.depth > 0 ) + hiddenLinks.Add( data.parent.node[data.linkIndex] ); + else + hiddenNodes.Add( data.node ); + } + + int id = state.initialNodeId + 1; + for( int i = 0; i < rootItem.children.Count; i++ ) + CalculateNewItemIdsAfterHideRecursive( rootItem.children[i], hiddenNodes, hiddenLinks, newExpandedItemIDs, newSelectedItemIDs, ref id ); + + for( int i = 0; i < ids.Count; i++ ) + { + ReferenceNodeData data = GetDataFromId( ids[i] ); + if( data.item.depth > 0 ) + { + // Can't remove by index here because if multiple sibling nodes are removed at once, the latter sibling nodes' linkIndex + // will be different than their actual sibling indices until this TreeView is refreshed + data.parent.node.RemoveLink( data.node ); + } + else + references.Remove( data.node ); + } + + SetSelection( newSelectedItemIDs ); + SetExpanded( newExpandedItemIDs ); + Reload(); + } + } + + private void CalculateNewItemIdsAfterHideRecursive( TreeViewItem item, List hiddenNodes, List hiddenLinks, List newExpandedItemIDs, List newSelectedItemIDs, ref int id ) + { + ReferenceNodeData data = GetDataFromId( item.id ); + if( hiddenNodes.Contains( data.node ) || ( data.parent != null && hiddenLinks.Contains( data.parent.node[data.linkIndex] ) ) ) + return; + + if( IsExpanded( item.id ) ) + newExpandedItemIDs.Add( id ); + if( IsSelected( item.id ) ) + newSelectedItemIDs.Add( id ); + + id++; + + if( item.hasChildren ) + { + for( int i = 0; i < item.children.Count; i++ ) + CalculateNewItemIdsAfterHideRecursive( item.children[i], hiddenNodes, hiddenLinks, newExpandedItemIDs, newSelectedItemIDs, ref id ); + } + } + + private void SelectFirstOccurrencesOfDuplicateSelection() + { + IList selection = GetSelection(); + if( selection.Count == 0 ) + return; + + HashSet selectedNodes = new HashSet(); + for( int i = selection.Count - 1; i >= 0; i-- ) + { + ReferenceNodeData data = GetDataFromId( selection[i] ); + if( data.isDuplicate ) + selectedNodes.Add( data.node ); + } + + List newSelection = new List( selection.Count ); + for( int i = 0; i < rootItem.children.Count; i++ ) + FindFirstOccurrencesOfSelectionRecursive( rootItem.children[i], selectedNodes, newSelection ); + + if( newSelection.Count > 0 ) + { + SetSelection( newSelection, TreeViewSelectionOptions.FireSelectionChanged | TreeViewSelectionOptions.RevealAndFrame ); + + if( treeType != TreeType.IsolatedView ) + EditorWindow.focusedWindow.SendEvent( new Event() { type = EventType.KeyDown, keyCode = KeyCode.F } ); // To actually frame the row when external scroll view is used + } + } + + private void FindFirstOccurrencesOfSelectionRecursive( TreeViewItem item, HashSet selectedNodes, List result ) + { + ReferenceNodeData data = GetDataFromId( item.id ); + if( !data.isDuplicate && selectedNodes.Remove( data.node ) ) + result.Add( item.id ); + + if( item.hasChildren ) + { + for( int i = 0; i < item.children.Count; i++ ) + FindFirstOccurrencesOfSelectionRecursive( item.children[i], selectedNodes, result ); + } + } + + private void ShowChildrenOfSelectedUnusedObjects() + { + IList selection = GetSelection(); + if( selection.Count == 0 ) + return; + + for( int i = selection.Count - 1; i >= 0; i-- ) + { + ReferenceNodeData data = GetDataFromId( selection[i] ); + if( data.node.usedState != ReferenceNode.UsedState.MixedCollapsed ) + continue; + + data.node.usedState = ReferenceNode.UsedState.MixedExpanded; + + Object unityObject = data.node.UnityObject; + if( !unityObject ) + continue; + + string assetPath = AssetDatabase.GetAssetPath( unityObject ); + if( string.IsNullOrEmpty( assetPath ) ) + { + foreach( Object obj in usedObjectsSet ) + { + if( obj && obj is GameObject && obj != unityObject && ( (GameObject) obj ).transform.IsChildOf( ( (GameObject) unityObject ).transform ) ) + { + ReferenceNode childNode = new ReferenceNode() { nodeObject = obj }; + childNode.InitializeRecursively(); + data.node.AddLinkTo( childNode, "USED" ); + } + } + } + else + { + foreach( Object obj in usedObjectsSet ) + { + if( obj && AssetDatabase.GetAssetPath( obj ) == assetPath ) + { + ReferenceNode childNode = new ReferenceNode() { nodeObject = obj }; + childNode.InitializeRecursively(); + data.node.AddLinkTo( childNode, "USED" ); + } + } + } + } + + Reload(); + } + + private void ShowChildrenOfSelectionInNewWindow() + { + IList selection = SortItemIDsInRowOrder( GetSelection() ); + if( selection.Count == 0 ) + return; + + List selectedNodes = new List( selection.Count ); + for( int i = 0; i < selection.Count; i++ ) + { + ReferenceNodeData data = GetDataFromId( selection[i] ); + if( data.node.NumberOfOutgoingLinks > 0 && !selectedNodes.Contains( data.node ) ) + selectedNodes.Add( data.node ); + } + + if( selectedNodes.Count > 0 ) + { + SearchResultTreeView isolatedTreeView = new SearchResultTreeView( new SearchResultTreeViewState(), selectedNodes, TreeType.IsolatedView, null, hideDuplicateRows, hideReduntantPrefabVariantLinks, false ); + isolatedTreeView.ExpandMainReferences(); + + SearchResultTreeViewIsolatedView.Show( new Vector2( EditorWindow.focusedWindow.position.width, Mathf.Max( isolatedTreeView.totalHeight, EditorGUIUtility.singleLineHeight * 5f ) + 1f ), isolatedTreeView, new GUIContent( selectedNodes[0].Label + ( selectedNodes.Count <= 1 ? "" : ( " (and " + ( selectedNodes.Count - 1 ) + " more)" ) ) ) ); + } + } + + private void RefreshSelectedNodes( IList selectedIds ) + { + selectedReferenceNodes.Clear(); + selectedReferenceNodesHierarchyIds.Clear(); + selectedReferenceNodesHierarchyIndirectIds.Clear(); + + for( int i = 0; i < selectedIds.Count; i++ ) + { + ReferenceNodeData data = GetDataFromId( selectedIds[i] ); + + selectedReferenceNodes.Add( data.node ); + selectedReferenceNodesHierarchyIds.Add( selectedIds[i] ); + + if( data.item.parent == null ) + continue; + + TreeViewItem linkItem = data.item; + for( TreeViewItem parentItem = linkItem.parent; parentItem.depth >= 0; parentItem = parentItem.parent ) + { + selectedReferenceNodesHierarchyIds.Add( parentItem.id ); + + List parentItemChildren = parentItem.children; + for( int j = 0; parentItemChildren[j] != linkItem; j++ ) + selectedReferenceNodesHierarchyIndirectIds.Add( parentItemChildren[j].id ); + + linkItem = parentItem; + } + } + } + + private void ShowTooltipDelayed() + { + if( EditorApplication.timeSinceStartup >= customTooltipShowTime ) + { + EditorApplication.update -= ShowTooltipDelayed; + + if( GetRows().Contains( hoveredData.item ) ) // Make sure that the hovered item is still a part of the tree (e.g. it might have been removed with middle mouse button) + SearchResultTooltip.Show( hoveredDataRect, hoveredData.tooltipText ); + } + } + + public void CancelDelayedTooltip() + { + EditorApplication.update -= ShowTooltipDelayed; + } + + public void GetRowStateWithId( int id, out bool isFirstRow, out bool isLastRow, out bool isExpanded, out bool canExpand ) + { + if( isTreeViewEmpty ) + { + isFirstRow = isLastRow = true; + isExpanded = canExpand = false; + + return; + } + + IList rows = GetRows(); + for( int i = 0; i < rows.Count; i++ ) + { + if( rows[i].id == id ) + { + isFirstRow = ( i <= 0 ); + isLastRow = ( i >= rows.Count - 1 ); + isExpanded = rows[i].hasChildren && IsExpanded( id ); + canExpand = rows[i].hasChildren && !IsExpanded( id ); + + return; + } + } + + isFirstRow = isLastRow = isExpanded = canExpand = false; + } + + public bool GetRowRectWithId( int id, out Rect rect ) + { + IList rows = GetRows(); + for( int i = 0; i < rows.Count; i++ ) + { + if( rows[i].id == id ) + { + rect = GetRowRect( i ); + return true; + } + } + + rect = new Rect(); + return false; + } + + public Rect SelectFirstRowAndReturnRect() + { + SetSelection( new int[1] { GetRows()[0].id }, TreeViewSelectionOptions.FireSelectionChanged ); + return GetRowRect( 0 ); + } + + public Rect SelectLastRowAndReturnRect() + { + IList rows = GetRows(); + SetSelection( new int[1] { rows[rows.Count - 1].id }, TreeViewSelectionOptions.FireSelectionChanged ); + return GetRowRect( rows.Count - 1 ); + } + + private void HighlightSearchTermInString( StringBuilder sb, string str ) + { + int prevSearchOccurrenceIndex = 0, searchOccurrenceIndex = 0; + while( ( searchOccurrenceIndex = textComparer.IndexOf( str, state.searchTerm, searchOccurrenceIndex, textCompareOptions ) ) >= 0 ) + { + sb.Append( str, prevSearchOccurrenceIndex, searchOccurrenceIndex - prevSearchOccurrenceIndex ); + sb.Append( highlightedSearchTextColor ).Append( "" ); + sb.Append( str, searchOccurrenceIndex, state.searchTerm.Length ); + sb.Append( "" ).Append( "" ); + + searchOccurrenceIndex += state.searchTerm.Length; + prevSearchOccurrenceIndex = searchOccurrenceIndex; + } + + if( prevSearchOccurrenceIndex < str.Length ) + sb.Append( str, prevSearchOccurrenceIndex, str.Length - prevSearchOccurrenceIndex ); + } + + public void OnSettingsChanged( bool resetHighlightedSearchTextColor, bool resetTooltipDescriptionsTextColor ) + { + hoveredData = null; + + if( !resetHighlightedSearchTextColor && !resetTooltipDescriptionsTextColor ) + return; + + if( resetHighlightedSearchTextColor ) + highlightedSearchTextColor = ""; + + for( int i = idToNodeDataLookup.Count - 1; i >= 0; i-- ) + { + if( isSearching && resetHighlightedSearchTextColor ) + idToNodeDataLookup[i].item.displayName = ""; + if( resetTooltipDescriptionsTextColor ) + idToNodeDataLookup[i].ResetTooltip(); + } + } + } + + public class SearchResultTreeViewIsolatedView : EditorWindow + { + private SearchResultTreeView treeView; + private bool shouldRepositionSelf = true; + + public static void Show( Vector2 preferredSize, SearchResultTreeView treeView, GUIContent title ) + { + SearchResultTreeViewIsolatedView window = CreateInstance(); + window.treeView = treeView; + window.titleContent = title; + window.Show(); + + window.minSize = new Vector2( 150f, Mathf.Min( preferredSize.y, EditorGUIUtility.singleLineHeight * 2f ) ); + window.position = new Rect( new Vector2( -9999f, -9999f ), preferredSize ); + window.Repaint(); + } + + private void OnEnable() + { + wantsMouseMove = wantsMouseEnterLeaveWindow = true; + } + + private void OnGUI() + { + if( treeView == null ) // After domain reload + Close(); + else + { + treeView.OnGUI( GUILayoutUtility.GetRect( 0f, 100000f, 0f, 100000f ) ); + + if( shouldRepositionSelf ) + { + float preferredHeight = GUILayoutUtility.GetLastRect().height; + if( preferredHeight > 10f ) + { + Vector2 size = position.size; + position = Utilities.GetScreenFittedRect( new Rect( GUIUtility.GUIToScreenPoint( Event.current.mousePosition ) + new Vector2( size.x * -0.5f, 10f ), size ) ); + + shouldRepositionSelf = false; + GUIUtility.ExitGUI(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTreeView.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTreeView.cs.meta new file mode 100644 index 0000000..94363a2 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/SearchResultTreeView.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: b3903f1d3149e9b4990e49206f8255c8 +timeCreated: 1638690728 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/Utilities.cs b/Assets/Plugins/AssetUsageDetector/Editor/Utilities.cs new file mode 100644 index 0000000..f1c6661 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/Utilities.cs @@ -0,0 +1,565 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +#if UNITY_2018_1_OR_NEWER +using Unity.Collections; +#endif +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; +#if UNITY_2018_3_OR_NEWER && !UNITY_2021_2_OR_NEWER +using PrefabStage = UnityEditor.Experimental.SceneManagement.PrefabStage; +using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility; +#endif + +namespace AssetUsageDetectorNamespace +{ + public static class Utilities + { + // A set of commonly used Unity types + private static readonly HashSet primitiveUnityTypes = new HashSet() + { + typeof( string ), typeof( Vector4 ), typeof( Vector3 ), typeof( Vector2 ), typeof( Rect ), + typeof( Quaternion ), typeof( Color ), typeof( Color32 ), typeof( LayerMask ), typeof( Bounds ), + typeof( Matrix4x4 ), typeof( AnimationCurve ), typeof( Gradient ), typeof( RectOffset ), + typeof( bool[] ), typeof( byte[] ), typeof( sbyte[] ), typeof( char[] ), typeof( decimal[] ), + typeof( double[] ), typeof( float[] ), typeof( int[] ), typeof( uint[] ), typeof( long[] ), + typeof( ulong[] ), typeof( short[] ), typeof( ushort[] ), typeof( string[] ), + typeof( Vector4[] ), typeof( Vector3[] ), typeof( Vector2[] ), typeof( Rect[] ), + typeof( Quaternion[] ), typeof( Color[] ), typeof( Color32[] ), typeof( LayerMask[] ), typeof( Bounds[] ), + typeof( Matrix4x4[] ), typeof( AnimationCurve[] ), typeof( Gradient[] ), typeof( RectOffset[] ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ), typeof( List ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ), typeof( List ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ), typeof( List ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ), +#if UNITY_2017_2_OR_NEWER + typeof( Vector3Int ), typeof( Vector2Int ), typeof( RectInt ), typeof( BoundsInt ), + typeof( Vector3Int[] ), typeof( Vector2Int[] ), typeof( RectInt[] ), typeof( BoundsInt[] ), + typeof( List ), typeof( List ), typeof( List ), typeof( List ) +#endif + }; + + private static readonly string reflectionNamespace = typeof( Assembly ).Namespace; +#if UNITY_2018_1_OR_NEWER + private static readonly string nativeCollectionsNamespace = typeof( NativeArray ).Namespace; +#endif + + private static MethodInfo screenFittedRectGetter; + + private static readonly HashSet folderContentsSet = new HashSet(); + + internal static readonly StringBuilder stringBuilder = new StringBuilder( 400 ); + + public static readonly GUILayoutOption GL_EXPAND_WIDTH = GUILayout.ExpandWidth( true ); + public static readonly GUILayoutOption GL_EXPAND_HEIGHT = GUILayout.ExpandHeight( true ); + public static readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25 ); + public static readonly GUILayoutOption GL_WIDTH_100 = GUILayout.Width( 100 ); + public static readonly GUILayoutOption GL_WIDTH_250 = GUILayout.Width( 250 ); + public static readonly GUILayoutOption GL_HEIGHT_0 = GUILayout.Height( 0 ); + public static readonly GUILayoutOption GL_HEIGHT_2 = GUILayout.Height( 2 ); + public static readonly GUILayoutOption GL_HEIGHT_30 = GUILayout.Height( 30 ); + public static readonly GUILayoutOption GL_HEIGHT_35 = GUILayout.Height( 35 ); + public static readonly GUILayoutOption GL_HEIGHT_40 = GUILayout.Height( 40 ); + + private static GUIStyle m_boxGUIStyle; // GUIStyle used to draw the results of the search + public static GUIStyle BoxGUIStyle + { + get + { + if( m_boxGUIStyle == null ) + { + m_boxGUIStyle = new GUIStyle( EditorStyles.helpBox ) + { + alignment = TextAnchor.MiddleCenter, + font = EditorStyles.label.font, + richText = true + }; + + Color textColor = GUI.skin.button.normal.textColor; + m_boxGUIStyle.normal.textColor = textColor; + m_boxGUIStyle.hover.textColor = textColor; + m_boxGUIStyle.focused.textColor = textColor; + m_boxGUIStyle.active.textColor = textColor; + +#if !UNITY_2019_1_OR_NEWER || UNITY_2019_3_OR_NEWER + // On 2019.1 and 2019.2 versions, GUI.skin.button.fontSize returns 0 on some devices + // https://forum.unity.com/threads/asset-usage-detector-find-references-to-an-asset-object-open-source.408134/page-3#post-7285954 + m_boxGUIStyle.fontSize = ( m_boxGUIStyle.fontSize + GUI.skin.button.fontSize ) / 2; +#endif + } + + return m_boxGUIStyle; + } + } + + // Check if object is an asset or a Scene object + public static bool IsAsset( this object obj ) + { + return obj is Object && AssetDatabase.Contains( (Object) obj ); + } + + public static bool IsAsset( this Object obj ) + { + return AssetDatabase.Contains( obj ); + } + + // Check if object is a folder asset + public static bool IsFolder( this Object obj ) + { + return obj is DefaultAsset && AssetDatabase.IsValidFolder( AssetDatabase.GetAssetPath( obj ) ); + } + + // Returns an enumerator to iterate through all asset paths in the folder + public static IEnumerable EnumerateFolderContents( Object folderAsset ) + { + string[] folderContents = AssetDatabase.FindAssets( "", new string[] { AssetDatabase.GetAssetPath( folderAsset ) } ); + if( folderContents == null ) + return new EmptyEnumerator(); + + folderContentsSet.Clear(); + for( int i = 0; i < folderContents.Length; i++ ) + { + string filePath = AssetDatabase.GUIDToAssetPath( folderContents[i] ); + if( !string.IsNullOrEmpty( filePath ) && !AssetDatabase.IsValidFolder( filePath ) ) + folderContentsSet.Add( filePath ); + } + + return folderContentsSet; + } + + public static void GetObjectsToSelectAndPing( this Object obj, out Object selection, out Object pingTarget ) + { + if( obj == null || obj.Equals( null ) ) + { + selection = pingTarget = null; + return; + } + + if( obj is Component ) + obj = ( (Component) obj ).gameObject; + + selection = pingTarget = obj; + + if( obj.IsAsset() ) + { + if( obj is GameObject ) + { + // Pinging a prefab only works if the pinged object is the root of the prefab or a direct child of it. Pinging any grandchildren + // of the prefab doesn't work; in which case, traverse the parent hierarchy until a pingable parent is reached +#if UNITY_2018_3_OR_NEWER + Transform objTR = ( (GameObject) obj ).transform.root; + + PrefabAssetType prefabAssetType = PrefabUtility.GetPrefabAssetType( objTR.gameObject ); + if( prefabAssetType == PrefabAssetType.Regular || prefabAssetType == PrefabAssetType.Variant ) + { + string assetPath = AssetDatabase.GetAssetPath( objTR.gameObject ); + PrefabStage openPrefabStage = PrefabStageUtility.GetCurrentPrefabStage(); +#if UNITY_2020_1_OR_NEWER + if( openPrefabStage != null && openPrefabStage.stageHandle.IsValid() && assetPath == openPrefabStage.assetPath ) +#else + if( openPrefabStage != null && openPrefabStage.stageHandle.IsValid() && assetPath == openPrefabStage.prefabAssetPath ) +#endif + { + GameObject prefabStageGO = FollowSymmetricHierarchy( (GameObject) obj, ( (GameObject) obj ).transform.root.gameObject, openPrefabStage.prefabContentsRoot ); + if( prefabStageGO != null ) + { + objTR = prefabStageGO.transform; + selection = objTR.gameObject; + } + } +#if UNITY_2019_1_OR_NEWER + else if( obj != objTR.gameObject ) + selection = objTR.gameObject; +#endif + } + else if( prefabAssetType == PrefabAssetType.Model ) + { + objTR = ( (GameObject) obj ).transform; + while( objTR.parent != null && objTR.parent.parent != null ) + objTR = objTR.parent; + } +#else + Transform objTR = ( (GameObject) obj ).transform; + while( objTR.parent != null && objTR.parent.parent != null ) + objTR = objTR.parent; +#endif + + pingTarget = objTR.gameObject; + } + else if( ( obj.hideFlags & ( HideFlags.HideInInspector | HideFlags.HideInHierarchy ) ) != HideFlags.None ) + { + // Can't ping assets that are hidden from Project window (e.g. animator states of AnimatorController), ping the main asset at that path instead + pingTarget = AssetDatabase.LoadMainAssetAtPath( AssetDatabase.GetAssetPath( obj ) ); + } + else if( !AssetDatabase.IsMainAsset( obj ) && Array.IndexOf( AssetDatabase.LoadAllAssetRepresentationsAtPath( AssetDatabase.GetAssetPath( obj ) ), obj ) < 0 ) + { + // VFX Graph assets' nodes are serialized as part of the graph but they are invisible in the Project window even though their hideFlags is None (I don't know how) + pingTarget = AssetDatabase.LoadMainAssetAtPath( AssetDatabase.GetAssetPath( obj ) ); + } + } + } + + // We are passing "go"s root Transform to thisRoot parameter. If we use go.transform.root instead, when we are in prefab mode on + // newer Unity versions, it points to the preview scene at the root of the prefab stage instead of pointing to the actual root of "go" + public static GameObject FollowSymmetricHierarchy( this GameObject go, GameObject thisRoot, GameObject symmetricRoot ) + { + Transform target = go.transform; + Transform root1 = thisRoot.transform; + Transform root2 = symmetricRoot.transform; + while( root1 != target ) + { + Transform temp = target; + while( temp.parent != root1 ) + temp = temp.parent; + + Transform newRoot2; + int siblingIndex = temp.GetSiblingIndex(); + if( siblingIndex < root2.childCount ) + { + newRoot2 = root2.GetChild( siblingIndex ); + if( newRoot2.name != temp.name ) + newRoot2 = root2.Find( temp.name ); + } + else + newRoot2 = root2.Find( temp.name ); + + if( newRoot2 == null ) + return null; + + root2 = newRoot2; + root1 = temp; + } + + return root2.gameObject; + } + + // Returns -1 if t1 is above t2 in Hierarchy, 1 if t1 is below t2 in Hierarchy and 0 if they are the same object + public static int CompareHierarchySiblingIndices( Transform t1, Transform t2 ) + { + Transform parent1 = t1.parent; + Transform parent2 = t2.parent; + + if( parent1 == parent2 ) + return t1.GetSiblingIndex() - t2.GetSiblingIndex(); + + int deltaHierarchyDepth = 0; + for( ; parent1; parent1 = parent1.parent ) + deltaHierarchyDepth++; + for( ; parent2; parent2 = parent2.parent ) + deltaHierarchyDepth--; + + for( ; deltaHierarchyDepth > 0; deltaHierarchyDepth-- ) + { + t1 = t1.parent; + if( t1 == t2 ) + return 1; + } + for( ; deltaHierarchyDepth < 0; deltaHierarchyDepth++ ) + { + t2 = t2.parent; + if( t1 == t2 ) + return -1; + } + + while( t1.parent != t2.parent ) + { + t1 = t1.parent; + t2 = t2.parent; + } + + return t1.GetSiblingIndex() - t2.GetSiblingIndex(); + } + + // Check if the field is serializable + public static bool IsSerializable( this FieldInfo fieldInfo ) + { + // See Serialization Rules: https://docs.unity3d.com/Manual/script-Serialization.html + if( fieldInfo.IsInitOnly ) + return false; + +#if UNITY_2019_3_OR_NEWER + // SerializeReference makes even System.Object fields serializable + if( Attribute.IsDefined( fieldInfo, typeof( SerializeReference ) ) ) + return true; +#endif + + if( ( !fieldInfo.IsPublic || fieldInfo.IsNotSerialized ) && !Attribute.IsDefined( fieldInfo, typeof( SerializeField ) ) ) + return false; + + return IsTypeSerializable( fieldInfo.FieldType ); + } + + // Check if the property is serializable + public static bool IsSerializable( this PropertyInfo propertyInfo ) + { + return IsTypeSerializable( propertyInfo.PropertyType ); + } + + // Check if type is serializable + private static bool IsTypeSerializable( Type type ) + { + // see Serialization Rules: https://docs.unity3d.com/Manual/script-Serialization.html + if( typeof( Object ).IsAssignableFrom( type ) ) + return true; + + if( type.IsArray ) + { + if( type.GetArrayRank() != 1 ) + return false; + + type = type.GetElementType(); + + if( typeof( Object ).IsAssignableFrom( type ) ) + return true; + } + else if( type.IsGenericType ) + { + // Generic types are allowed on 2020.1 and later +#if UNITY_2020_1_OR_NEWER + if( type.GetGenericTypeDefinition() == typeof( List<> ) ) + { + type = type.GetGenericArguments()[0]; + + if( typeof( Object ).IsAssignableFrom( type ) ) + return true; + } +#else + if( type.GetGenericTypeDefinition() != typeof( List<> ) ) + return false; + + type = type.GetGenericArguments()[0]; + + if( typeof( Object ).IsAssignableFrom( type ) ) + return true; +#endif + } + +#if !UNITY_2020_1_OR_NEWER + if( type.IsGenericType ) + return false; +#endif + + return Attribute.IsDefined( type, typeof( SerializableAttribute ), false ); + } + + // Check if instances of this type should be searched for references + public static bool IsIgnoredUnityType( this Type type ) + { + if( type.IsPrimitive || primitiveUnityTypes.Contains( type ) || type.IsEnum ) + return true; + +#if UNITY_2018_1_OR_NEWER + // Searching NativeArrays for reference can throw InvalidOperationException if the collection is disposed + if( type.Namespace == nativeCollectionsNamespace ) + return true; +#endif + + // Searching assembly variables for reference throws InvalidCastException on .NET 4.0 runtime + if( typeof( Type ).IsAssignableFrom( type ) || type.Namespace == reflectionNamespace ) + return true; + + // Searching pointers or ref variables for reference throws ArgumentException + if( type.IsPointer || type.IsByRef ) + return true; + + return false; + } + + // Get function for a field + public static VariableGetVal CreateGetter( this FieldInfo fieldInfo, Type type ) + { + // Commented the IL generator code below because it might actually be slower than simply using reflection + // Credit: https://www.codeproject.com/Articles/14560/Fast-Dynamic-Property-Field-Accessors + //DynamicMethod dm = new DynamicMethod( "Get" + fieldInfo.Name, fieldInfo.FieldType, new Type[] { typeof( object ) }, type ); + //ILGenerator il = dm.GetILGenerator(); + //// Load the instance of the object (argument 0) onto the stack + //il.Emit( OpCodes.Ldarg_0 ); + //// Load the value of the object's field (fi) onto the stack + //il.Emit( OpCodes.Ldfld, fieldInfo ); + //// return the value on the top of the stack + //il.Emit( OpCodes.Ret ); + + //return (VariableGetVal) dm.CreateDelegate( typeof( VariableGetVal ) ); + + return fieldInfo.GetValue; + } + + // Get function for a property + public static VariableGetVal CreateGetter( this PropertyInfo propertyInfo ) + { + // Can't use PropertyWrapper (which uses CreateDelegate) for property getters of structs + if( propertyInfo.DeclaringType.IsValueType ) + { + return !propertyInfo.CanRead ? (VariableGetVal) null : ( obj ) => + { + try + { + return propertyInfo.GetValue( obj, null ); + } + catch + { + // Property getters may return various kinds of exceptions if their backing fields are not initialized (yet) + return null; + } + }; + } + + Type GenType = typeof( PropertyWrapper<,> ).MakeGenericType( propertyInfo.DeclaringType, propertyInfo.PropertyType ); + return ( (IPropertyAccessor) Activator.CreateInstance( GenType, propertyInfo.GetGetMethod( true ) ) ).GetValue; + } + + // Check if all open scenes are saved (not dirty) + public static bool AreScenesSaved() + { + for( int i = 0; i < SceneManager.sceneCount; i++ ) + { + Scene scene = SceneManager.GetSceneAt( i ); + if( scene.isDirty || string.IsNullOrEmpty( scene.path ) ) + return false; + } + + return true; + } + + // Returns file extension in lowercase (period not included) + public static string GetFileExtension( string path ) + { + int extensionIndex = path.LastIndexOf( '.' ); + if( extensionIndex < 0 || extensionIndex >= path.Length - 1 ) + return ""; + + stringBuilder.Length = 0; + for( extensionIndex++; extensionIndex < path.Length; extensionIndex++ ) + { + char ch = path[extensionIndex]; + if( ch >= 65 && ch <= 90 ) // A-Z + ch += (char) 32; // Converted to a-z + + stringBuilder.Append( ch ); + } + + return stringBuilder.ToString(); + } + + // Draw horizontal line inside OnGUI + public static void DrawSeparatorLine() + { + GUILayout.Space( 4f ); + GUILayout.Box( "", GL_HEIGHT_2, GL_EXPAND_WIDTH ); + GUILayout.Space( 4f ); + } + + // Restricts the given Rect within the screen's bounds + public static Rect GetScreenFittedRect( Rect originalRect ) + { + if( screenFittedRectGetter == null ) + screenFittedRectGetter = typeof( EditorWindow ).Assembly.GetType( "UnityEditor.ContainerWindow" ).GetMethod( "FitRectToScreen", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ); + + return (Rect) screenFittedRectGetter.Invoke( null, new object[3] { originalRect, true, true } ); + } + + // Check if all the objects inside the list are null + public static bool IsEmpty( this List objectsToSearch ) + { + if( objectsToSearch == null ) + return true; + + for( int i = 0; i < objectsToSearch.Count; i++ ) + { + if( objectsToSearch[i].obj != null && !objectsToSearch[i].obj.Equals( null ) ) + return false; + } + + return true; + } + + // Check if all the objects inside the list are null + public static bool IsEmpty( this List objects ) + { + if( objects == null ) + return true; + + for( int i = 0; i < objects.Count; i++ ) + { + if( objects[i] != null && !objects[i].Equals( null ) ) + return false; + } + + return true; + } + + // Check if all the objects that are enumerated are null + public static bool IsEmpty( this IEnumerable objects ) + { + if( objects == null ) + return true; + + using( IEnumerator enumerator = objects.GetEnumerator() ) + { + while( enumerator.MoveNext() ) + { + if( enumerator.Current != null && !enumerator.Current.Equals( null ) ) + return false; + } + } + + return true; + } + + // Returns true is str starts with prefix + public static bool StartsWithFast( this string str, string prefix ) + { + int aLen = str.Length; + int bLen = prefix.Length; + int ap = 0; int bp = 0; + while( ap < aLen && bp < bLen && str[ap] == prefix[bp] ) + { + ap++; + bp++; + } + + return bp == bLen; + } + + // Returns true is str ends with postfix + public static bool EndsWithFast( this string str, string postfix ) + { + int ap = str.Length - 1; + int bp = postfix.Length - 1; + while( ap >= 0 && bp >= 0 && str[ap] == postfix[bp] ) + { + ap--; + bp--; + } + + return bp < 0; + } + + public static bool ContainsFast( this List list, T element ) + { + if( !( element is ValueType ) ) + { + for( int i = list.Count - 1; i >= 0; i-- ) + { + if( ReferenceEquals( list[i], element ) ) + return true; + } + } + else + { + for( int i = list.Count - 1; i >= 0; i-- ) + { + if( element.Equals( list[i] ) ) + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/Utilities.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/Utilities.cs.meta new file mode 100644 index 0000000..2daf8ac --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/Utilities.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52b272b7591fb90499916205261524e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/Editor/VariableGetter.cs b/Assets/Plugins/AssetUsageDetector/Editor/VariableGetter.cs new file mode 100644 index 0000000..87c8fa9 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/VariableGetter.cs @@ -0,0 +1,83 @@ +using System; +using System.Reflection; +using System.Text; +using UnityEngine; + +namespace AssetUsageDetectorNamespace +{ + // Delegate to get the value of a variable (either field or property) + public delegate object VariableGetVal( object obj ); + + // Custom struct to hold a variable, its important properties and its getter function + public struct VariableGetterHolder + { + public readonly MemberInfo variable; + public readonly bool isSerializable; + private readonly VariableGetVal getter; + + public string Name { get { return variable.Name; } } + public bool IsProperty { get { return variable is PropertyInfo; } } + + public VariableGetterHolder( FieldInfo fieldInfo, VariableGetVal getter, bool isSerializable ) + { + this.variable = fieldInfo; + this.isSerializable = isSerializable; + this.getter = getter; + } + + public VariableGetterHolder( PropertyInfo propertyInfo, VariableGetVal getter, bool isSerializable ) + { + this.variable = propertyInfo; + this.isSerializable = isSerializable; + this.getter = getter; + } + + public object Get( object obj ) + { + try + { + return getter( obj ); + } + catch( Exception e ) + { + StringBuilder sb = Utilities.stringBuilder; + sb.Length = 0; + sb.Append( "Error while getting the value of (" ).Append( IsProperty ? ( (PropertyInfo) variable ).PropertyType : ( (FieldInfo) variable ).FieldType ).Append( ") " ) + .Append( variable.DeclaringType ).Append( "." ).Append( Name ).Append( ": " ).Append( e ); + + Debug.LogError( sb.ToString() ); + return null; + } + } + } + + // Credit: http://stackoverflow.com/questions/724143/how-do-i-create-a-delegate-for-a-net-property + public interface IPropertyAccessor + { + object GetValue( object source ); + } + + // A wrapper class for properties to get their values more efficiently + public class PropertyWrapper : IPropertyAccessor where TObject : class + { + private readonly Func getter; + + public PropertyWrapper( MethodInfo getterMethod ) + { + getter = (Func) Delegate.CreateDelegate( typeof( Func ), getterMethod ); + } + + public object GetValue( object obj ) + { + try + { + return getter( (TObject) obj ); + } + catch + { + // Property getters may return various kinds of exceptions if their backing fields are not initialized (yet) + return null; + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/Editor/VariableGetter.cs.meta b/Assets/Plugins/AssetUsageDetector/Editor/VariableGetter.cs.meta new file mode 100644 index 0000000..b78cc0e --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/Editor/VariableGetter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e94c83e8b850514ca0217aeff1491a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/AssetUsageDetector/README.txt b/Assets/Plugins/AssetUsageDetector/README.txt new file mode 100644 index 0000000..f962275 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/README.txt @@ -0,0 +1,4 @@ += Asset Usage Detector (v2.5.3) = + +Documentation: https://github.com/yasirkula/UnityAssetUsageDetector +E-mail: yasirkula@gmail.com \ No newline at end of file diff --git a/Assets/Plugins/AssetUsageDetector/README.txt.meta b/Assets/Plugins/AssetUsageDetector/README.txt.meta new file mode 100644 index 0000000..6670523 --- /dev/null +++ b/Assets/Plugins/AssetUsageDetector/README.txt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac528f1751f33a647a45caeff6a9344b +timeCreated: 1520032521 +licenseType: Store +TextScriptImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Character/PlayerAvatar.prefab b/Assets/Prefabs/Character/PlayerAvatar.prefab index 01e2c3e..73093d1 100644 --- a/Assets/Prefabs/Character/PlayerAvatar.prefab +++ b/Assets/Prefabs/Character/PlayerAvatar.prefab @@ -243,6 +243,10 @@ PrefabInstance: propertyPath: m_ClientVisualization value: objectReference: {fileID: 7209204667172237188} + - target: {fileID: 7420593339233078707, guid: 0d2d836e2e83b754fa1a1c4022d6d65d, type: 3} + propertyPath: uIStateDisplayHandler + value: + objectReference: {fileID: 2576537793715222015} - target: {fileID: 7690172137830037487, guid: 0d2d836e2e83b754fa1a1c4022d6d65d, type: 3} propertyPath: m_Enabled value: 1 @@ -700,6 +704,7 @@ MonoBehaviour: m_DisplayHealth: 1 m_DisplayName: 1 m_UIStatePrefab: {fileID: -1943162842029199943, guid: 2b07482491a17964380023240087ce16, type: 3} + m_UIState: {fileID: 0} m_NetworkHealthState: {fileID: 7751377510591478590} m_NetworkNameState: {fileID: -8197545831548902967} m_BaseHP: {fileID: 0} diff --git a/Assets/Prefabs/UI/UIStateDisplay.prefab b/Assets/Prefabs/UI/UIStateDisplay.prefab index 06da694..4af4908 100644 --- a/Assets/Prefabs/UI/UIStateDisplay.prefab +++ b/Assets/Prefabs/UI/UIStateDisplay.prefab @@ -1,5 +1,137 @@ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: +--- !u!1 &1494894897330052786 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9018035827392313124} + - component: {fileID: 5538368923929045334} + - component: {fileID: 1678916513842311094} + - component: {fileID: 2486900489436361486} + m_Layer: 5 + m_Name: CrossButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &9018035827392313124 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1494894897330052786} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8467790018694481767} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 0.5, y: 0} + m_AnchoredPosition: {x: 20, y: 0} + m_SizeDelta: {x: 27.375, y: 27.25} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &5538368923929045334 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1494894897330052786} + m_CullTransparentMesh: 1 +--- !u!114 &1678916513842311094 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1494894897330052786} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: -187649311, guid: 730b0201dba9701479b075c3cc0c9d0c, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2486900489436361486 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1494894897330052786} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1678916513842311094} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 2833502134534602906} + m_TargetAssemblyTypeName: SwapConfirmationPanel, Unity.BossRoom.Gameplay + m_MethodName: OnDeclineButtonPressed + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 --- !u!1 &1678686474824094549 GameObject: m_ObjectHideFlags: 0 @@ -28,11 +160,12 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: + - {fileID: 4684960432987786827} - {fileID: 2113819744356243524} - {fileID: 254771808888734010} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} @@ -77,13 +210,413 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: baba913bade45a84486ef7a3f715299e, type: 3} m_Name: m_EditorClassIdentifier: + swapConfirmationPanel: {fileID: 2833502134534602906} + playerName: m_UIName: {fileID: 2963962490385848884} m_UIHealth: {fileID: 6385511411654384321} +--- !u!1 &3198388185403688655 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6284368815799428365} + - component: {fileID: 7797288771431175908} + - component: {fileID: 799420244153101338} + m_Layer: 0 + m_Name: Text (TMP) (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6284368815799428365 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3198388185403688655} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8467790018694481767} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 1} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: 10} + m_SizeDelta: {x: 100, y: 18} + m_Pivot: {x: 0.5, y: 1} +--- !u!222 &7797288771431175908 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3198388185403688655} + m_CullTransparentMesh: 1 +--- !u!114 &799420244153101338 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3198388185403688655} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: SWAP? + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 1a8c97d4cbe5134499b26527f8609c7e, type: 2} + m_sharedMaterial: {fileID: 2100000, guid: 9ba25c6f3254c4a4cb00e7407e967916, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 18 + m_fontSizeBase: 22 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 1 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &3585409544634893643 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8467790018694481767} + - component: {fileID: 6839476388405747010} + m_Layer: 5 + m_Name: SwapConfirmationPanelNew + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &8467790018694481767 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3585409544634893643} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 6284368815799428365} + - {fileID: 8966934417392808048} + - {fileID: 9018035827392313124} + m_Father: {fileID: 4684960432987786827} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 44.8} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &6839476388405747010 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3585409544634893643} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!1 &4554001488283748877 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8966934417392808048} + - component: {fileID: 2844404199525477580} + - component: {fileID: 7911038305113171221} + - component: {fileID: 7027835586965462475} + m_Layer: 5 + m_Name: TickButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8966934417392808048 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4554001488283748877} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8467790018694481767} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 0.5, y: 0} + m_AnchoredPosition: {x: -20, y: 0} + m_SizeDelta: {x: 27.375, y: 27.25} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &2844404199525477580 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4554001488283748877} + m_CullTransparentMesh: 1 +--- !u!114 &7911038305113171221 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4554001488283748877} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 1065262782, guid: 730b0201dba9701479b075c3cc0c9d0c, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &7027835586965462475 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4554001488283748877} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 7911038305113171221} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 2833502134534602906} + m_TargetAssemblyTypeName: SwapConfirmationPanel, Unity.BossRoom.Gameplay + m_MethodName: OnAcceptButtonPressed + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 +--- !u!1 &8235012725380876694 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4684960432987786827} + - component: {fileID: 3288483462710877394} + - component: {fileID: 2833502134534602906} + m_Layer: 5 + m_Name: SwapConfirmationPanelManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4684960432987786827 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8235012725380876694} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8467790018694481767} + m_Father: {fileID: 7502187991751756120} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 50, y: -20} + m_SizeDelta: {x: 100, y: 40} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &3288483462710877394 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8235012725380876694} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!114 &2833502134534602906 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8235012725380876694} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e799529efab1c9d4ab780bfd693439d1, type: 3} + m_Name: + m_EditorClassIdentifier: + confirmationPanel: {fileID: 3585409544634893643} + swapwithname: {fileID: 799420244153101338} --- !u!1001 &1161329279327836704 PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 7502187991751756120} m_Modifications: - target: {fileID: 330850194307782490, guid: ef40e983a46e6194c83cdaa6aa2462d6, type: 3} @@ -183,6 +716,9 @@ PrefabInstance: value: 0 objectReference: {fileID: 0} m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: ef40e983a46e6194c83cdaa6aa2462d6, type: 3} --- !u!224 &254771808888734010 stripped RectTransform: @@ -205,12 +741,17 @@ PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: + serializedVersion: 3 m_TransformParent: {fileID: 7502187991751756120} m_Modifications: - target: {fileID: 5566236674185336343, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_Name value: UIName objectReference: {fileID: 0} + - target: {fileID: 5566236674185336343, guid: bfbfd55622765a341a87916c843208ab, type: 3} + propertyPath: m_IsActive + value: 1 + objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_Pivot.x value: 0.5 @@ -229,7 +770,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_AnchorMax.y - value: 1 + value: 0 objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_AnchorMin.x @@ -237,7 +778,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_AnchorMin.y - value: 1 + value: 0 objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_SizeDelta.x @@ -277,11 +818,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_AnchoredPosition.x - value: 50 + value: 0 objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_AnchoredPosition.y - value: 10 + value: 0 objectReference: {fileID: 0} - target: {fileID: 7997572053438227093, guid: bfbfd55622765a341a87916c843208ab, type: 3} propertyPath: m_LocalEulerAnglesHint.x @@ -296,6 +837,9 @@ PrefabInstance: value: 0 objectReference: {fileID: 0} m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: bfbfd55622765a341a87916c843208ab, type: 3} --- !u!224 &2113819744356243524 stripped RectTransform: diff --git a/Assets/Scenes/BossRoom.unity b/Assets/Scenes/BossRoom.unity index adb106d..c94596b 100644 --- a/Assets/Scenes/BossRoom.unity +++ b/Assets/Scenes/BossRoom.unity @@ -8597,11 +8597,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 1676734515771252668, guid: 0193228de87741d40a42e561901c9083, type: 3} propertyPath: m_LocalRotation.w - value: 0.85294455 + value: 0.8529446 objectReference: {fileID: 0} - target: {fileID: 1676734515771252668, guid: 0193228de87741d40a42e561901c9083, type: 3} propertyPath: m_LocalRotation.x - value: 0.3943448 + value: 0.39434478 objectReference: {fileID: 0} - target: {fileID: 1676734515771252668, guid: 0193228de87741d40a42e561901c9083, type: 3} propertyPath: m_LocalRotation.y @@ -8609,7 +8609,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 1676734515771252668, guid: 0193228de87741d40a42e561901c9083, type: 3} propertyPath: m_LocalRotation.z - value: -0.14352979 + value: -0.14352977 objectReference: {fileID: 0} - target: {fileID: 1676734516302391364, guid: 0193228de87741d40a42e561901c9083, type: 3} propertyPath: m_UpdateMethod @@ -8758,15 +8758,15 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 203267159508449512, guid: 36b3ee75677a1544191c0ddaaadd8140, type: 3} propertyPath: m_LocalRotation.x - value: 0.39434484 + value: 0.3943448 objectReference: {fileID: 0} - target: {fileID: 203267159508449512, guid: 36b3ee75677a1544191c0ddaaadd8140, type: 3} propertyPath: m_LocalRotation.y - value: 0.3104465 + value: 0.31044644 objectReference: {fileID: 0} - target: {fileID: 203267159508449512, guid: 36b3ee75677a1544191c0ddaaadd8140, type: 3} propertyPath: m_LocalRotation.z - value: -0.1435298 + value: -0.14352976 objectReference: {fileID: 0} - target: {fileID: 203267159508449512, guid: 36b3ee75677a1544191c0ddaaadd8140, type: 3} propertyPath: m_LocalEulerAnglesHint.x @@ -9221,7 +9221,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!114 &1486746209 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs b/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs index 21096ba..ee8ad22 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs @@ -4,6 +4,7 @@ using Unity.BossRoom.ConnectionManagement; using Unity.BossRoom.Gameplay.Actions; using Unity.BossRoom.Gameplay.Configuration; using Unity.BossRoom.Gameplay.GameplayObjects.Character.AI; +using Unity.BossRoom.Gameplay.UI; using Unity.Multiplayer.Samples.BossRoom; using Unity.Netcode; using UnityEngine; @@ -146,40 +147,56 @@ namespace Unity.BossRoom.Gameplay.GameplayObjects.Character public bool IsOnAPlatform { get; private set; } = false; public bool IsCrow { get; private set; } = false; - - + public UIStateDisplayHandler uIStateDisplayHandler; + + void Awake() { m_ServerActionPlayer = new ServerActionPlayer(this); NetLifeState = GetComponent(); NetHealthState = GetComponent(); m_State = GetComponent(); + uIStateDisplayHandler= GetComponent(); } //Hazim [Rpc(SendTo.Everyone)] - private void ShowSwapConfirmationPanelClientRpc() + private void ShowSwapConfirmationPanelClientRpc(string name) { if (NetworkManager.Singleton.LocalClientId == OwnerClientId) { - // Show the confirmation panel for the specific player - var panel = FindObjectOfType(); + var uistate= uIStateDisplayHandler.m_UIState; + var panel = uistate.swapConfirmationPanel; + if (panel != null) { + uistate.swapConfirmationPanel.swapwithname.text = "Swap with " + name; panel.ShowPanel(this); // Pass the current ServerCharacter reference } else { Debug.LogError("SwapConfirmationPanel not found in the scene!"); } + //// Show the confirmation panel for the specific player + //var panel = FindObjectOfType(); + //if (panel != null) + //{ + // panel.ShowPanel(this); // Pass the current ServerCharacter reference + //} + //else + //{ + // Debug.LogError("SwapConfirmationPanel not found in the scene!"); + //} + + } } public void SetAsCrow(bool status) { IsCrow = status; } - + public void SetTargetPlatform(int platformId) { TargetPlatformId = platformId; @@ -200,12 +217,12 @@ namespace Unity.BossRoom.Gameplay.GameplayObjects.Character { SetOnPlatform(false); } - + public void SetOnPlatform(bool status) { IsOnAPlatform = status; } - + private void HandleOccupiedPlatform(Platform currentPlatform) { @@ -239,15 +256,15 @@ namespace Unity.BossRoom.Gameplay.GameplayObjects.Character } - - + + [Rpc(SendTo.Server, RequireOwnership = false)] - public void NotifySwapRequestRpc(ulong initiatingPlayerId) + public void NotifySwapRequestRpc(ulong initiatingPlayerId,string name) { PendingSwapRequest = initiatingPlayerId; // Notify all clients except the server, filtered by target - ShowSwapConfirmationPanelClientRpc(); + ShowSwapConfirmationPanelClientRpc(name); Debug.Log($"Swap request received from Player {initiatingPlayerId}. Waiting for confirmation."); } @@ -295,7 +312,7 @@ namespace Unity.BossRoom.Gameplay.GameplayObjects.Character } //Hazim - + public override void OnNetworkSpawn() { CrowManager.Instance.OnPlayerSpawned(this); diff --git a/Assets/Scripts/Gameplay/SwapConfirmationPanel.cs b/Assets/Scripts/Gameplay/SwapConfirmationPanel.cs index 955693c..b3d7019 100644 --- a/Assets/Scripts/Gameplay/SwapConfirmationPanel.cs +++ b/Assets/Scripts/Gameplay/SwapConfirmationPanel.cs @@ -5,10 +5,11 @@ using Unity.Multiplayer.Samples.BossRoom; using Unity.Netcode; using UnityEngine; using UnityEngine.UI; - +using TMPro; public class SwapConfirmationPanel : MonoBehaviour { [SerializeField] private GameObject confirmationPanel; + public TextMeshProUGUI swapwithname; private ServerCharacter associatedCharacter; // Reference to the correct ServerCharacter instance public void ShowPanel(ServerCharacter character) diff --git a/Assets/Scripts/Gameplay/UI/UIStateDisplay.cs b/Assets/Scripts/Gameplay/UI/UIStateDisplay.cs index 26e6346..c6705ea 100644 --- a/Assets/Scripts/Gameplay/UI/UIStateDisplay.cs +++ b/Assets/Scripts/Gameplay/UI/UIStateDisplay.cs @@ -1,4 +1,5 @@ using System; +using Unity.BossRoom.Gameplay.GameplayObjects.Character; using Unity.BossRoom.Utils; using Unity.Netcode; using UnityEngine; @@ -10,6 +11,8 @@ namespace Unity.BossRoom.Gameplay.UI /// public class UIStateDisplay : MonoBehaviour { + public SwapConfirmationPanel swapConfirmationPanel; + public string playerName; [SerializeField] UIName m_UIName; @@ -20,6 +23,10 @@ namespace Unity.BossRoom.Gameplay.UI { m_UIName.gameObject.SetActive(true); m_UIName.Initialize(networkedName); + playerName=networkedName.Value.ToString(); + //var servercharacterName = GetComponentInParent(); + //servercharacterName.name = playerName; + swapConfirmationPanel.gameObject.name += networkedName.Value; } public void DisplayHealth(NetworkVariable networkedHealth, int maxValue) diff --git a/Assets/Scripts/Gameplay/UI/UIStateDisplayHandler.cs b/Assets/Scripts/Gameplay/UI/UIStateDisplayHandler.cs index b9c3730..3b29b28 100644 --- a/Assets/Scripts/Gameplay/UI/UIStateDisplayHandler.cs +++ b/Assets/Scripts/Gameplay/UI/UIStateDisplayHandler.cs @@ -29,7 +29,7 @@ namespace Unity.BossRoom.Gameplay.UI UIStateDisplay m_UIStatePrefab; // spawned in world (only one instance of this) - UIStateDisplay m_UIState; + public UIStateDisplay m_UIState; RectTransform m_UIStateRectTransform; diff --git a/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs b/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs index b5a096b..8a4d288 100644 --- a/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs +++ b/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs @@ -558,7 +558,7 @@ namespace Unity.BossRoom.Gameplay.UserInput if (targetNetObj.TryGetComponent(out ServerCharacter targetCharacter)) { // Initiate the swap - targetCharacter.NotifySwapRequestRpc(m_ServerCharacter.NetworkObjectId); + targetCharacter.NotifySwapRequestRpc(m_ServerCharacter.NetworkObjectId,m_ServerCharacter.uIStateDisplayHandler.m_UIState.playerName); Debug.Log($"Swap request sent to {targetCharacter.name}."); isCharacterClicked = true; break; // Exit the loop after initiating the swap diff --git a/Assets/options.png b/Assets/options.png new file mode 100644 index 0000000..bae37f1 Binary files /dev/null and b/Assets/options.png differ diff --git a/Assets/options.png.meta b/Assets/options.png.meta new file mode 100644 index 0000000..093a80c --- /dev/null +++ b/Assets/options.png.meta @@ -0,0 +1,158 @@ +fileFormatVersion: 2 +guid: 730b0201dba9701479b075c3cc0c9d0c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + 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: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 2 + 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: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + 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: 3 + 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: + - serializedVersion: 2 + name: tick + rect: + serializedVersion: 2 + x: 31 + y: 147 + width: 219 + height: 218 + alignment: 0 + pivot: {x: 0, y: 0} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: cb9db3cf4f25a424db73d054f5083d70 + internalID: 1065262782 + vertices: [] + indices: + edges: [] + weights: [] + - serializedVersion: 2 + name: cross + rect: + serializedVersion: 2 + x: 262 + y: 147 + width: 219 + height: 218 + alignment: 0 + pivot: {x: 0, y: 0} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: 570ede4f598fcfe44ab1b710195bcbd4 + internalID: -187649311 + vertices: [] + indices: + edges: [] + weights: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: e9791250060e17a4db2bb0ae9ee6b852 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: + cross: -187649311 + tick: 1065262782 + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: