Files
5.24BuYu/Packages/com.kybernetik.inspector-gadgets/Editor/Inspector/MissingScriptWindow.cs
T
2026-03-10 15:36:10 +08:00

578 lines
22 KiB
C#

// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
// The only way to currently determine the name and serialized fields of a missing script is via a custom inspector
// for MonoBehaviour scripts (but not other classes that inherit from it) which needs each object to be selected in
// turn. If anyone knows of a better way to do it, please email mail@kybernetik.com.au.
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A utility for tracking down and fixing missing script references.
/// </summary>
internal sealed class MissingScriptWindow : EditorWindow, IHasCustomMenu
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
public static MissingScriptWindow Instance { get; private set; }
public bool AutoDestroy { get; private set; }
// Gathered Assets.
[NonSerialized] private SceneSetup[] _OriginalSceneSetup;
[NonSerialized] private readonly List<string> MissingAssetPaths = new List<string>();
[NonSerialized] private readonly List<GameObject> MissingPrefabs = new List<GameObject>();
[NonSerialized] private readonly List<string> ScenePaths = new List<string>();
[NonSerialized] private readonly List<GameObject> SceneObjects = new List<GameObject>();
[NonSerialized] private int _SceneObjectIndex;
// Selected Target Details.
[NonSerialized] private Object _Target;
[NonSerialized] private SerializedProperty _ScriptProperty;
[NonSerialized] private bool _HasSerializedProperties;
[NonSerialized] private int[] _SimilarityRatings;
[NonSerialized] private Vector2 _ScrollPosition;
[NonSerialized] private bool _HasLoadedAnotherScene;
[NonSerialized] private bool _ConfirmResetScene;
/************************************************************************************************************************/
public bool HasTarget => _ScriptProperty != null;
/************************************************************************************************************************/
private static MonoScript[] _AllScripts;
public static MonoScript[] AllScripts
{
get
{
if (_AllScripts == null)
_AllScripts = MonoImporter.GetAllRuntimeMonoScripts();
return _AllScripts;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region EditorWindow Methods
/************************************************************************************************************************/
private void OnEnable()
{
titleContent = new GUIContent("Missing Script Window");
Instance = this;
Selection.selectionChanged += OnSelectionChanged;
}
/************************************************************************************************************************/
private void OnGUI()
{
const float MinWidth = 300;
var fieldWidth = position.width < MinWidth ?
position.width * 0.5f :
MinWidth * 0.5f + Mathf.Pow(position.width - MinWidth, 0.65f);
EditorGUIUtility.labelWidth = position.width - fieldWidth;
if (_OriginalSceneSetup == null)
GatherScenesAndMissingAsssets();
if (EditorUtility.scriptCompilationFailed)
EditorGUILayout.HelpBox("The project currently has compile errors which should be fixed" +
" before trying to fix missing script references.", MessageType.Warning);
if (DoSelectNextButtonGUI())
return;
if (_ScriptProperty == null)
{
DoGatheredAssetsGUI();
return;
}
_ScrollPosition = GUILayout.BeginScrollView(_ScrollPosition);
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_ScriptProperty, true);
if (EditorGUI.EndChangeCheck())
{
var script = _ScriptProperty.objectReferenceValue as MonoScript;
if (script != null && script.GetClass() != null)
_ScriptProperty.serializedObject.ApplyModifiedProperties();
else
_ScriptProperty.serializedObject.Update();
}
if (!(_Target is null) && GUILayout.Button("Destroy Component"))
DestroyTargetAndSelectNext();
DoMissingScriptButtons();
GUILayout.EndScrollView();
}
/************************************************************************************************************************/
private void Update()
{
if (AutoDestroy)
{
if (_Target is null)
{
AutoDestroy = false;
return;
}
DestroyTargetAndSelectNext();
}
}
/************************************************************************************************************************/
private void OnDestroy()
{
Selection.selectionChanged -= OnSelectionChanged;
ResetSceneSetup();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
void IHasCustomMenu.AddItemsToMenu(GenericMenu menu)
{
menu.AddItem(new GUIContent("Destroy All Missing Scripts"), false, DestroyAllMissingScripts);
}
/************************************************************************************************************************/
private void DestroyAllMissingScripts()
{
const string Message = "Are you sure you want to destroy all missing scripts" +
" in scenes and assets everywhere in your project?" +
"\n\nYou cannot undo this action.";
if (!EditorUtility.DisplayDialog("Destroy All Missing Scripts?", Message, "Destroy", "Cancel"))
return;
AutoDestroy = true;
DeleteAllMissingAssets();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
private void OnSelectionChanged()
{
if (_HasLoadedAnotherScene)
_ConfirmResetScene = true;
Repaint();
}
/************************************************************************************************************************/
private void GatherScenesAndMissingAsssets()
{
_OriginalSceneSetup = EditorSceneManager.GetSceneManagerSetup();
_HasLoadedAnotherScene = false;
_ConfirmResetScene = false;
MissingAssetPaths.Clear();
MissingPrefabs.Clear();
ScenePaths.Clear();
var guids = AssetDatabase.FindAssets($"t:{nameof(Object)}");
for (int i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (asset == null)
{
MissingAssetPaths.Add(path);
}
}
guids = AssetDatabase.FindAssets($"t:{nameof(GameObject)}");
for (int i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (HasAnyMissingComponents(prefab))
{
MissingPrefabs.Add(prefab);
}
}
guids = AssetDatabase.FindAssets($"t:{nameof(SceneAsset)}");
for (int i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
ScenePaths.Add(path);
}
}
/************************************************************************************************************************/
private void DoGatheredAssetsGUI()
{
_ScrollPosition = GUILayout.BeginScrollView(_ScrollPosition);
DoMissingAssetsGUI();
GUILayout.Label("Prefabs with missing scripts: " + MissingPrefabs.Count, EditorStyles.boldLabel);
for (int i = 0; i < MissingPrefabs.Count; i++)
{
var prefab = MissingPrefabs[i];
EditorGUILayout.ObjectField(AssetDatabase.GetAssetPath(prefab), prefab, typeof(GameObject), true);
}
GUILayout.Label("Scenes: " + ScenePaths.Count, EditorStyles.boldLabel);
GUILayout.Label("We won't know if they contain any missing scripts until we open them.", EditorStyles.wordWrappedLabel);
for (int i = 0; i < ScenePaths.Count; i++)
{
var scene = ScenePaths[i];
EditorGUILayout.ObjectField(scene, AssetDatabase.LoadAssetAtPath<SceneAsset>(scene), typeof(SceneAsset), true);
}
GUILayout.EndScrollView();
GUILayout.Label(
"Assets are removed from these lists as they are selected." +
"\nTo begin again, simply click this button:",
EditorStyles.wordWrappedLabel);
if (GUILayout.Button("Re-Gather Assets"))
{
GatherScenesAndMissingAsssets();
}
}
/************************************************************************************************************************/
private void DoMissingAssetsGUI()
{
GUILayout.Label("Assets with missing scripts: " + MissingAssetPaths.Count, EditorStyles.boldLabel);
if (MissingAssetPaths.Count == 0)
return;
GUILayout.BeginHorizontal();
GUILayout.Label("These assets can't be automatically selected" +
" because Unity doesn't give us any way to reference them.", EditorStyles.wordWrappedLabel);
if (GUILayout.Button("Delete All", EditorStyles.miniButton, Editor.IGEditorUtils.DontExpandWidth))
{
const string Message = "Are you sure you want to delete all assets with missing scripts?" +
"\n\nYou cannot undo this action.";
if (EditorUtility.DisplayDialog("Delete all assets with missing scripts?", Message, "Delete", "Cancel"))
{
DeleteAllMissingAssets();
GUIUtility.ExitGUI();
}
}
GUILayout.EndHorizontal();
for (int i = 0; i < MissingAssetPaths.Count; i++)
{
GUILayout.BeginHorizontal();
var path = MissingAssetPaths[i];
GUILayout.Label(MissingAssetPaths[i], GUI.skin.textArea);
var area = GUILayoutUtility.GetLastRect();
var currentEvent = Event.current;
if (currentEvent.type == EventType.MouseUp &&
area.Contains(currentEvent.mousePosition))
{
path = Path.GetDirectoryName(path);
var asset = AssetDatabase.LoadAssetAtPath(path, typeof(Object));
SelectObject(asset);
}
if (GUILayout.Button("Delete", EditorStyles.miniButton, Editor.IGEditorUtils.DontExpandWidth))
{
var message = path + "\n\nYou cannot undo this action.";
if (EditorUtility.DisplayDialog("Delete asset?", message, "Delete", "Cancel"))
{
AssetDatabase.DeleteAsset(path);
MissingAssetPaths.RemoveAt(i);
GUIUtility.ExitGUI();
}
}
GUILayout.EndHorizontal();
}
}
/************************************************************************************************************************/
private void DeleteAllMissingAssets()
{
for (int i = 0; i < MissingAssetPaths.Count; i++)
{
AssetDatabase.DeleteAsset(MissingAssetPaths[i]);
}
MissingAssetPaths.Clear();
}
/************************************************************************************************************************/
private void DoMissingScriptButtons()
{
if (_SimilarityRatings == null)
return;
var scripts = AllScripts;
for (int i = scripts.Length - 1; i >= 0; i--)
{
var similarity = _SimilarityRatings[i];
if (similarity <= 0)
{
if (i == scripts.Length - 1)
{
GUILayout.Label(_HasSerializedProperties
? "No similar scripts found."
: "Unable to determine the missing script's name or find any serialized properties to compare other scripts against.",
EditorStyles.wordWrappedLabel);
}
continue;
}
var script = scripts[i];
var prefix = similarity == int.MaxValue ?
"[Name Match] " :
$"[{similarity}] ";
if (GUILayout.Button(prefix + script.name))
{
_ScriptProperty.objectReferenceValue = script;
_ScriptProperty.serializedObject.ApplyModifiedProperties();
SelectNextMissingScript();
return;
}
}
}
/************************************************************************************************************************/
private void SelectNextMissingScript()
{
_ScriptProperty = null;
// Next Prefab.
for (int i = MissingPrefabs.Count - 1; i >= 0; i--)
{
var prefab = MissingPrefabs[i];
MissingPrefabs.RemoveAt(i);
if (HasAnyMissingComponents(prefab))
{
SelectObject(prefab);
return;
}
}
NextScene:
if (_SceneObjectIndex >= SceneObjects.Count)
{
if (ScenePaths.Count > 0)
{
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
return;
var scene = ScenePaths[ScenePaths.Count - 1];
ScenePaths.RemoveAt(ScenePaths.Count - 1);
EditorSceneManager.OpenScene(scene);
_HasLoadedAnotherScene = true;
GatherSceneObjects();
_SceneObjectIndex = -1;
// Continue on to pick a scene object.
}
else
{
// No scenes left, so we're done.
ResetSceneSetup();
return;
}
}
NextSceneObject:
_SceneObjectIndex++;
if (_SceneObjectIndex < SceneObjects.Count)
{
var sceneObject = SceneObjects[_SceneObjectIndex];
if (HasAnyMissingComponents(sceneObject))
{
SelectObject(sceneObject);
return;
}
else
{
goto NextSceneObject;
}
}
else
{
SceneObjects.Clear();
goto NextScene;
}
}
private void GatherSceneObjects()
{
SceneObjects.Clear();
foreach (var gameObject in Resources.FindObjectsOfTypeAll<GameObject>())
{
if ((gameObject.hideFlags & HideFlags.HideAndDontSave) != 0)
continue;
if (EditorUtility.IsPersistent(gameObject.transform.root.gameObject))
continue;
SceneObjects.Add(gameObject);
}
}
/************************************************************************************************************************/
private void DestroyTargetAndSelectNext()
{
MissingScriptEditor.DestroyProperly(_Target, true);
ClearTarget();
SelectNextMissingScript();
}
/************************************************************************************************************************/
private static void SelectObject(Object target)
{
Selection.activeObject = null;
EditorApplication.delayCall += () =>
Selection.activeObject = target;
}
/************************************************************************************************************************/
public void SetScripts(Object target, SerializedProperty scriptProperty, bool hasSerializedProperties, int[] similarityRatings)
{
_Target = target;
_ScriptProperty = scriptProperty;
_HasSerializedProperties = hasSerializedProperties;
_SimilarityRatings = similarityRatings;
Array.Sort(similarityRatings, AllScripts);
GetWindow<MissingScriptWindow>();
}
/************************************************************************************************************************/
public void ClearTarget()
{
_Target = null;
_ScriptProperty = null;
_SimilarityRatings = null;
}
/************************************************************************************************************************/
private bool DoSelectNextButtonGUI()
{
if (MissingPrefabs.Count > 0 || ScenePaths.Count > 0 || _SceneObjectIndex < SceneObjects.Count)
{
if (GUILayout.Button("Select Next Missing Script"))
{
SelectNextMissingScript();
return true;
}
}
else
{
if (GUILayout.Button("Close"))
{
Close();
return true;
}
}
return false;
}
/************************************************************************************************************************/
private static readonly List<MonoBehaviour> Components = new List<MonoBehaviour>();
private static bool HasAnyMissingComponents(GameObject gameObject)
{
gameObject.GetComponents(Components);
for (int i = 0; i < Components.Count; i++)
{
if (Components[i] == null)
return true;
}
return false;
}
/************************************************************************************************************************/
private void ResetSceneSetup()
{
_HasLoadedAnotherScene = false;
// If the current scene setup has changed, ask if the user wants to return to what they started with.
var sceneSetup = EditorSceneManager.GetSceneManagerSetup();
if (sceneSetup.Length != _OriginalSceneSetup.Length)
goto ReturnToOriginalSetup;
for (int i = 0; i < sceneSetup.Length; i++)
{
if (sceneSetup[i].path != _OriginalSceneSetup[i].path)
goto ReturnToOriginalSetup;
}
return;
ReturnToOriginalSetup:
if (_ConfirmResetScene && !EditorUtility.DisplayDialog("Restore Original Scene Setup",
"Would you like to return to the scene you had open when you opened the Scripty Hunter?", "Restore", "Do Nothing"))
return;
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
return;
EditorSceneManager.RestoreSceneManagerSetup(_OriginalSceneSetup);
}
/************************************************************************************************************************/
}
}
#endif