forked from Shardstone/trail-into-darkness
280 lines
11 KiB
C#
280 lines
11 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
#endif
|
|
|
|
namespace InspectorToolkit {
|
|
public class RequiredAttribute : PropertyAttribute, IPropertyCondition {
|
|
|
|
public enum Scope {
|
|
|
|
None,
|
|
SelfOrChild,
|
|
SceneOnly,
|
|
SceneOrParentPrefabOnly
|
|
// ProjectOnly
|
|
|
|
/*
|
|
* ProjectOnly has a quirk I can't figure out. If you reference a prefab in the project then it works fine.
|
|
* However if you also have an instance of that prefab if the same scene/prefab then it will become a reference to that instance
|
|
* instead of the one in the project. Even the icon changes in Unity. I think this may be a Unity Bug.
|
|
*/
|
|
|
|
}
|
|
|
|
public readonly Scope scope;
|
|
|
|
public RequiredAttribute() {
|
|
this.scope = Scope.None;
|
|
}
|
|
|
|
public RequiredAttribute(Scope scope) {
|
|
this.scope = scope;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
public bool DoesPropertyMeetCondition(SerializedProperty property, out string description) {
|
|
description = string.Empty;
|
|
|
|
if (property.propertyType != SerializedPropertyType.ObjectReference) {
|
|
description = $"{property.name} is not an ObjectReference";
|
|
return false;
|
|
}
|
|
|
|
bool hasValue = property.objectReferenceValue != null;
|
|
|
|
if (scope == Scope.None) {
|
|
if (!hasValue) {
|
|
description = $"{property.name} requires a value";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (scope == Scope.SelfOrChild) {
|
|
if (!hasValue) {
|
|
description = $"{property.name} requires a value";
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetTargetGameObject(property, out GameObject parentGameObject)) {
|
|
description = $"{property.name} does not have a GameObject parent";
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetGameObject(property.objectReferenceValue, out GameObject childGameObject)) {
|
|
description = $"{property.name} has a non-GameObject value";
|
|
return false;
|
|
}
|
|
|
|
if (!IsObjectSelfOrChild(parentGameObject, childGameObject)) {
|
|
description = $"{property.name} value is not itself or it's child";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (scope == Scope.SceneOnly) {
|
|
if (property.serializedObject.targetObject is ScriptableObject) {
|
|
description = $"{property.name} SceneOnly is not supported on ScriptableObjects";
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) {
|
|
description = $"{property.name} does not have a GameObject parent";
|
|
return false;
|
|
}
|
|
|
|
// scene is invalid - this is a prefab in the project.
|
|
if (thisGameObject.scene.IsValid() == false) {
|
|
// scene isn't valid, can't check
|
|
description = $"{property.name} has a reference to GameObject not in a Scene";
|
|
return true;
|
|
}
|
|
|
|
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
// scene is valid - prefab stage exists - this is a prefab
|
|
if (prefabStage != null) {
|
|
return true;
|
|
}
|
|
|
|
if (!hasValue) {
|
|
description = $"{property.name} requires a value";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (scope == Scope.SceneOrParentPrefabOnly) {
|
|
if (property.serializedObject.targetObject is ScriptableObject) {
|
|
description = $"{property.name} SceneOrParentPrefabOnly is not supported on ScriptableObjects";
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) {
|
|
description = $"{property.name} does not have a GameObject parent";
|
|
return false;
|
|
}
|
|
|
|
// scene is invalid - this is a prefab in the project.
|
|
if (thisGameObject.scene.IsValid() == false) {
|
|
// scene isn't valid, can't check
|
|
description = $"{property.name} has a reference to GameObject not in a Scene";
|
|
return true;
|
|
}
|
|
|
|
bool hasTargetGameObject = TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject);
|
|
|
|
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
// scene is valid - prefab stage exists - this is a prefab
|
|
if (prefabStage != null) {
|
|
// if the prefab open has this requirement on its root object
|
|
if (prefabStage.prefabContentsRoot == thisGameObject) {
|
|
if (hasTargetGameObject) {
|
|
// if the value assigned is a gameObject
|
|
if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) {
|
|
// then fail if it's within the prefab
|
|
description = $"{property.name} requires an object external to its prefab / hierarchy";
|
|
return false;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
return true; // if we don't have a target then it is valid (only inside this prefab of ourselves)
|
|
}
|
|
}
|
|
|
|
// if the prefab open is a different prefab, then we're valid if a value is assigned
|
|
if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) {
|
|
description = $"{property.name} requires an object external to its prefab / hierarchy";
|
|
return false;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// not in a prefab, value must be set to an object not within the scope of this attribute
|
|
if (!(hasValue && !IsObjectSelfOrChild(thisGameObject, targetGameObject))) {
|
|
description = $"{property.name} requires an object external to its prefab / hierarchy";
|
|
return false;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
See note above why this is not enabled
|
|
|
|
if (scope == Scope.ProjectOnly) {
|
|
if (!hasValue) {
|
|
return false;
|
|
}
|
|
|
|
bool isPersistent = EditorUtility.IsPersistent(property.objectReferenceValue);
|
|
bool objectHasAssetPath = !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(property.objectReferenceValue));
|
|
bool hasNoScene = false;
|
|
bool isPrefabRoot = false;
|
|
bool isInAssetDatabase = AssetDatabase.Contains(property.objectReferenceValue);
|
|
|
|
if (TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject)) {
|
|
isPrefabRoot = PrefabUtility.IsOutermostPrefabInstanceRoot(targetGameObject);
|
|
if (targetGameObject.scene.IsValid() == false) {
|
|
// scene isn't valid, can't check
|
|
//return true;
|
|
hasNoScene = true;
|
|
}
|
|
}
|
|
|
|
// if the target has a path, then it is an asset and that is valid!
|
|
return objectHasAssetPath;
|
|
}*/
|
|
|
|
if (!hasValue) {
|
|
description = $"{property.name} requires a value";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryGetTargetGameObject(SerializedProperty property, out GameObject gameObject) {
|
|
return TryGetGameObject(property.serializedObject.targetObject, out gameObject);
|
|
}
|
|
|
|
private static bool TryGetGameObject(Object obj, out GameObject gameObject) {
|
|
if (obj == null) {
|
|
gameObject = null;
|
|
return false;
|
|
}
|
|
|
|
gameObject = obj switch {
|
|
Component component => component.gameObject,
|
|
GameObject targetGameObject => targetGameObject,
|
|
_ => null
|
|
};
|
|
return gameObject != null;
|
|
}
|
|
|
|
private static bool IsObjectSelfOrChild(GameObject parentObject, GameObject queryObject) {
|
|
if (parentObject == null || queryObject == null) {
|
|
return false;
|
|
}
|
|
|
|
return parentObject == queryObject || queryObject.transform.IsChildOf(parentObject.transform);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
namespace InspectorToolkit.Internal {
|
|
[CustomPropertyDrawer(typeof(RequiredAttribute))]
|
|
public class RequiredAttributePropertyDrawer : PropertyDrawer {
|
|
|
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
|
if (property.propertyType != SerializedPropertyType.ObjectReference) {
|
|
Debug.LogError($"Required should only be used on Unity Object references. Target='{property.serializedObject.targetObject.name}.{property.propertyPath}'");
|
|
EditorGUI.PropertyField(position, property, label);
|
|
return;
|
|
}
|
|
|
|
RequiredAttribute requiredAttribute = (RequiredAttribute)attribute;
|
|
|
|
Color guiColor = GUI.color;
|
|
|
|
if (!requiredAttribute.DoesPropertyMeetCondition(property, out string text)) {
|
|
RequiredUtil.LayoutRequired(ref position, text, false);
|
|
RequiredUtil.DrawRequired(ref position, text, false);
|
|
}
|
|
|
|
label = EditorGUI.BeginProperty(position, label, property);
|
|
position = EditorGUI.PrefixLabel(position, label);
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
EditorGUI.PropertyField(position, property, GUIContent.none);
|
|
|
|
if (EditorGUI.EndChangeCheck()) {
|
|
property.serializedObject.ApplyModifiedProperties();
|
|
}
|
|
|
|
EditorGUI.EndProperty();
|
|
|
|
GUI.color = guiColor;
|
|
}
|
|
|
|
}
|
|
}
|
|
#endif |