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

362 lines
14 KiB
C#

// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A custom Inspector for <see cref="MonoBehaviour"/> but not its children, so it should only ever get used on
/// missing scripts.
/// </summary>
#if !DISABLE_IG_MONOBEHAVIOUR_INSPECTOR
[CustomEditor(typeof(MonoBehaviour), isFallback = true)]
#endif
public class MissingScriptEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
[NonSerialized]
private SerializedProperty _ScriptProperty;
/// <summary>The target's "m_Script" property.</summary>
protected SerializedProperty ScriptProperty
{
get
{
GatherSerializedProperties();
return _ScriptProperty;
}
}
/************************************************************************************************************************/
[NonSerialized]
private List<SerializedProperty> _OtherProperties;
/// <summary>All of the target's properties other than "m_Script".</summary>
protected List<SerializedProperty> OtherProperties
{
get
{
GatherSerializedProperties();
return _OtherProperties;
}
}
/************************************************************************************************************************/
#region Property Gathering and Analysis
/************************************************************************************************************************/
/// <summary>
/// Gathers the target's properties. If its script is missing, this method tries to find other similar scripts.
/// </summary>
protected virtual void OnEnable()
{
if (target != null ||
MissingScriptWindow.Instance == null ||
ScriptProperty == null)
return;
if (MissingScriptWindow.Instance.AutoDestroy)
{
DestroyProperly(targets, false);
return;
}
if (MissingScriptWindow.Instance.HasTarget)
return;
// Get the referenced script's name (if possible).
var scriptReference = _ScriptProperty.objectReferenceValue;
var scriptName = scriptReference != null ? scriptReference.name : null;
var fields = new List<FieldInfo>();
var scripts = MissingScriptWindow.AllScripts;
var similarityRatings = new int[scripts.Length];
// Go through each script and calculate its similarity to the missing script.
for (int i = 0; i < scripts.Length; i++)
{
var script = scripts[i];
var scriptType = script.GetClass();
if (scriptType == null)
continue;
// Scripts with the same name have max similarity.
if (script.name == scriptName)
{
similarityRatings[i] = int.MaxValue;
continue;
}
// Otherwise count the number of fields with names and types matching the serialized properties.
fields.Clear();
GatherAllFields(scriptType, fields);
similarityRatings[i] = CountSimilarFields(fields);
}
var hasSerializedProperties = scriptName != null || _OtherProperties.Count > 0;
MissingScriptWindow.Instance.SetScripts(target, _ScriptProperty, hasSerializedProperties, similarityRatings);
}
/************************************************************************************************************************/
private Object[] _GatheredPropertiesFrom;
private void GatherSerializedProperties()
{
var targets = this.targets;
if (_OtherProperties != null)
{
if (_GatheredPropertiesFrom == null ||
_GatheredPropertiesFrom.Length != targets.Length)
goto Gather;
for (int i = 0; i < _GatheredPropertiesFrom.Length; i++)
if (!ReferenceEquals(_GatheredPropertiesFrom[i], targets[i]))
goto Gather;
return;
}
Gather:
if (_GatheredPropertiesFrom == null || _GatheredPropertiesFrom.Length != targets.Length)
_GatheredPropertiesFrom = new Object[targets.Length];
for (int i = 0; i < _GatheredPropertiesFrom.Length; i++)
{
_GatheredPropertiesFrom[i] = targets[i];
}
_OtherProperties = new List<SerializedProperty>();
try
{
var iterator = serializedObject.GetIterator();
if (!iterator.NextVisible(true))
return;
do
{
var property = iterator.Copy();
if (property.propertyPath == "m_Script")
_ScriptProperty = property;
else
_OtherProperties.Add(property);
}
while (iterator.NextVisible(false));
}
catch { }
if (_ScriptProperty == null && target is MonoBehaviour)
Debug.Log("Script property was not found on " + target, target);
}
/************************************************************************************************************************/
private static void GatherAllFields(Type type, List<FieldInfo> fields)
{
var typeFields = type.GetFields(IGEditorUtils.InstanceBindings);
for (int i = 0; i < typeFields.Length; i++)
{
var field = typeFields[i];
if (field.IsDefined(typeof(NonSerializedAttribute), true) ||
field.IsDefined(typeof(HideInInspector), true))
continue;
if (!field.IsPublic && !field.IsDefined(typeof(SerializeField), true))
continue;
fields.Add(field);
}
}
/************************************************************************************************************************/
private int CountSimilarFields(List<FieldInfo> fields)
{
var count = 0;
for (int i = 0; i < _OtherProperties.Count; i++)
{
var property = _OtherProperties[i];
for (int j = 0; j < fields.Count; j++)
{
var field = fields[j];
if (IsMatch(property, field))
{
count++;
break;
}
}
}
return count;
}
/************************************************************************************************************************/
private static bool IsMatch(SerializedProperty property, FieldInfo field)
{
if (field.Name != property.propertyPath)
return false;
switch (property.propertyType)
{
case SerializedPropertyType.Integer: return typeof(int).IsAssignableFrom(field.FieldType) || typeof(uint).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.Boolean: return field.FieldType == typeof(bool);
case SerializedPropertyType.Float: return typeof(float).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.String: return field.FieldType == typeof(string);
case SerializedPropertyType.Color: return field.FieldType == typeof(Color) || field.FieldType == typeof(Color32);
case SerializedPropertyType.ObjectReference: return typeof(Object).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.LayerMask: return field.FieldType == typeof(LayerMask);
case SerializedPropertyType.Enum: return field.FieldType.IsEnum;// This doesn't confirm that the enum type actually matches the field. Would need to check property.enumDisplayNames.
case SerializedPropertyType.Vector2: return field.FieldType == typeof(Vector2);
case SerializedPropertyType.Vector3: return field.FieldType == typeof(Vector3);
case SerializedPropertyType.Vector4: return field.FieldType == typeof(Vector4);
case SerializedPropertyType.Rect: return field.FieldType == typeof(Rect);
case SerializedPropertyType.Generic: return field.FieldType.HasElementType || typeof(IEnumerable).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.Character: return field.FieldType == typeof(char);
case SerializedPropertyType.AnimationCurve: return field.FieldType == typeof(AnimationCurve);
case SerializedPropertyType.Bounds: return field.FieldType == typeof(Bounds);
case SerializedPropertyType.Gradient: return field.FieldType == typeof(Gradient);
case SerializedPropertyType.Quaternion: return field.FieldType == typeof(Quaternion);
case SerializedPropertyType.Vector2Int: return field.FieldType == typeof(Vector2Int);
case SerializedPropertyType.Vector3Int: return field.FieldType == typeof(Vector3Int);
case SerializedPropertyType.RectInt: return field.FieldType == typeof(RectInt);
case SerializedPropertyType.BoundsInt: return field.FieldType == typeof(BoundsInt);
default:
case SerializedPropertyType.ArraySize:
case SerializedPropertyType.ExposedReference:
case SerializedPropertyType.FixedBufferSize:
break;
}
return false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Draws the target's inspector with a message indicating that the script is missing.
/// </summary>
public override void OnInspectorGUI()
{
if (target != null)
{
base.OnInspectorGUI();
return;
}
serializedObject.Update();
// Draw the script field and a help box.
GUI.enabled = true;
EditorGUILayout.PropertyField(ScriptProperty, true);
EditorGUILayout.HelpBox(EditorUtility.scriptCompilationFailed ?
"The associated script can not be loaded. Please fix any compile errors and assign a valid script." :
"The associated script can not be loaded. Please assign a valid script.", MessageType.Warning);
// Draw all other serialized properties disabled.
GUI.enabled = false;
for (int i = 0; i < _OtherProperties.Count; i++)
{
EditorGUILayout.PropertyField(_OtherProperties[i], true);
}
GUI.enabled = true;
// Apply modifications in case the user dragged a new script into the script field.
serializedObject.ApplyModifiedProperties();
if (GUILayout.Button(target is Component ? "Remove Component" : "Destroy This"))
{
EditorApplication.delayCall += () => DestroyProperly(targets, true);
}
if (GUILayout.Button("Find and Fix Missing Scripts"))
{
EditorWindow.GetWindow<MissingScriptWindow>();
OnEnable();
}
}
/************************************************************************************************************************/
/// <summary>
/// Indicates to the <see cref="MissingScriptWindow"/> that the target has been deselected.
/// </summary>
protected virtual void OnDisable()
{
if (MissingScriptWindow.Instance != null)
MissingScriptWindow.Instance.ClearTarget();
}
/************************************************************************************************************************/
/// <summary>Destroys the `targets`. Shows a confirmation dialog before destroying assets.</summary>
public static void DestroyProperly(Object[] targets, bool confirmAssetDeletion)
{
for (int i = 0; i < targets.Length; i++)
DestroyProperly(targets[i], confirmAssetDeletion);
}
/// <summary>Destroys the `target`. Shows a confirmation dialog before destroying assets.</summary>
public static void DestroyProperly(Object target, bool confirmAssetDeletion)
{
var assetPath = AssetDatabase.GetAssetPath(target);
if (!string.IsNullOrEmpty(assetPath))
{
var isMainAsset = AssetDatabase.IsMainAsset(target);
var message = isMainAsset ?
assetPath :
$"{target.name} (sub asset of '{assetPath}')";
message += "\n\nYou cannot undo this action.";
if (!confirmAssetDeletion || EditorUtility.DisplayDialog("Delete selected asset?", message, "Delete", "Cancel"))
{
if (isMainAsset)
{
AssetDatabase.DeleteAsset(assetPath);
}
else
{
DestroyImmediate(target, true);
AssetDatabase.ImportAsset(assetPath);
}
}
}
else
{
Undo.DestroyObjectImmediate(target);
}
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
/************************************************************************************************************************/
}
}
#endif